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>
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>