javascriptweb-animations

JavaScript Web Animation, delay is ignored when trying to reverse?


I've created a stack of animations using JavaScript Web Animations, which gradually fade out characters in e.g. a sentence. I've achieved this by applying a staggered delay property to the keyframe timing object for the web animation.

I then try to reverse this animation stack, however the delay seems to be completely ignored in this case, and all the animations happen at the same time.

Is this a bug or expected behavior (i.e. I would need to write a dedicated animation stack to make the last characters to disappear also the first one to appear again)?

const animationContainer = document.querySelector("#animation-container");
const buttonStart = document.querySelector("#button-start");
const buttonReverse = document.querySelector("#button-reverse");
const buttonPlaybackRate = document.querySelector("#button-playbackRate");
const buttonUpdatePlaybackRate = document.querySelector("#button-updatePlaybackRate");

const animationDefinitionDefault = {
  keyframes: [
    { opacity: "1" },
    { opacity: "0.1" },
  ],

  timing: {
    duration: 500,
    delay: 0,
    iterations: 1,
    easing: "linear",
    fill: "both",
  }
};


// Create our animations
const animationStack = [];
const characters = animationContainer.querySelectorAll("span");

// Create the animation for each character
characters.forEach((character, index) => {
  const delay = index * 250; // Stagger the animations
  const animationDefinition = structuredClone(animationDefinitionDefault);

  animationDefinition.timing.delay = delay;

  animationStack[index] = character.animate(...Object.values(animationDefinition));
  animationStack[index].pause();
});


// Start button
buttonStart.addEventListener("click", (event) => {
  animationStack.forEach(animation => {
    animation.playbackRate = 1;
    animation.play();
  });
});


// Reverse buttons
buttonReverse.addEventListener("click", (event) => {
  animationStack.forEach(animation => {
    animation.reverse();
  });
});

buttonPlaybackRate.addEventListener("click", (event) => {
  animationStack.forEach(animation => {
    animation.playbackRate = -1;
    animation.play();
  });
});

buttonUpdatePlaybackRate.addEventListener("click", (event) => {
  animationStack.forEach(animation => {
    animation.updatePlaybackRate(-1);
    animation.ready.then(() => {
      animation.play();
    });
  });
});
#animation-container {
  font-size: 20px;
  margin-bottom: 1em;
}

button {
  font-size: 16px;
}
<div id="animation-container">
  <span>1</span>
  <span>2</span>
  <span>3</span>
  <span>4</span>
  <span>5</span>
  <span>6</span>
</div>

<button type="button" id="button-start">Start</button>
<button type="button" id="button-reverse">reverse()</button>
<button type="button" id="button-playbackRate">playbackRate = -1</button>
<button type="button" id="button-updatePlaybackRate">updatePlaybackRate(-1)</button>


Solution

  • Answering my own question here. I assume it's not possible at all with the way the animations are set up. The .reverse() functionality would only work if the it would be a single animation, or if you could somehow "queue" or group animations, so that they know they're depending on each other (Babylon.js seems to have something like this?).

    So instead I had to set the delay property to 0 and modify the endDelay property, as apparently for playback in reverse, the endDelay takes over what delay does when being played forwards.

    Here's some modified example code:

    const animationContainer = document.querySelector("#animation-container");
    const buttonStart = document.querySelector("#button-start");
    const buttonReverse = document.querySelector("#button-reverse");
    
    const animationDefinitionDefault = {
      keyframes: [
        { opacity: "1" },
        { opacity: "0.1" },
      ],
    
      timing: {
        duration: 500,
        delay: 0,
        iterations: 1,
        easing: "linear",
        fill: "both",
      }
    };
    
    
    // Create our animations
    const animationStack = [];
    const originalDelays = [];
    const characters = animationContainer.querySelectorAll("span");
    
    
    // Create the animation for each character
    characters.forEach((character, index) => {
      const delay = index * 250; // Stagger the animations
      const animationDefinition = structuredClone(animationDefinitionDefault);
    
      // Store our original delay, so that it can be re-applied
      originalDelays.push(delay);
      animationDefinition.timing.delay = delay;
    
      animationStack[index] = character.animate(...Object.values(animationDefinition));
      animationStack[index].pause();
    });
    
    
    // Start button
    buttonStart.addEventListener("click", (event) => {
      animationStack.forEach((animation, index) => {
        // We need to reset any possible alterations done by the reverse call
        animation.effect.updateTiming({ delay: originalDelays[index], endDelay: 0 });
        animation.updatePlaybackRate(1);
        animation.ready.then(() => {
          animation.play();
        });
      });
    });
    
    
    // Reverse button
    buttonReverse.addEventListener("click", (event) => {
      const reverseDelays = originalDelays.toReversed();
    
      for ( let i = animationStack.length-1; i >= 0; i-- ) {
        // We need to update our delay and endDelay
        // endDelay is used instead of delay for playback in reverse
        animationStack[i].effect.updateTiming({ delay: 0, endDelay: reverseDelays[i] });
        animationStack[i].reverse();
      }
    });
    #animation-container {
      font-size: 20px;
      margin-bottom: 1em;
    }
    
    button {
      font-size: 16px;
    }
      <div id="animation-container">
        <span>1</span>
        <span>2</span>
        <span>3</span>
        <span>4</span>
        <span>5</span>
        <span>6</span>
      </div>
      
      <button type="button" id="button-start">Start</button>
      <button type="button" id="button-reverse">reverse()</button>