sp00n
sp00n

Reputation: 1098

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>

Upvotes: 0

Views: 66

Answers (2)

sp00n
sp00n

Reputation: 1098

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>

Upvotes: 2

Kosh
Kosh

Reputation: 18378

You might do it changing the direction property.

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.effect.updateTiming({
      direction: 'normal'
    });
    animation.play();
  });
});


// Reverse buttons
buttonReverse.addEventListener("click", (event) => {
  animationStack.forEach(animation => {
    animation.effect.updateTiming({
      direction: 'reverse'
    });
    animation.play();
  });
});

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>

Upvotes: 1

Related Questions