Scrubbing via the Web Animations API
A question appeared on a great Web Animations API article on CSS Tricks about whether or not the API would ever support scrubbing through an animation. As is often the case it does support that, but at a lower level (meaning you can’t just say
The Power of
Creating a player control like the scrubber is possible thanks to the API’s
currentTime property which is a read/write value that shows (or sets) what millisecond the animation is at. An animation with only one iteration will have a
currentTime with a max value of the
duration plus any delays.
Creating a scrubber for a single animation then becomes an exercise in
currentTime management. Things get more interesting as we want to scrub through multiple animations as a group, which is where we will focus (though what is covered here can be simplified to handle scrubbing one animation).
Our example focuses on five separate elements with one animation each (a
transform that moves each element to the right with a rotation) that all have the same duration. Each additionally has a
delay to stagger the animations. Since
currentTime includes delays, as the
delay gets bigger, so does the max
We add one useful property to the animations to counter the
endDelay. This does not currently have a CSS animation counterpart, but effectively does what
delay does… just at the end. This is particularly important when thinking about
currentTime. If I can set up all my animations to start playing at the same time and stop playing at the same (again, both delays will be used to caluclate the max
currentTime value), then I can use a single control/scrubber to affect all the animations at once.
Now all the animations will kick off at effectively the same time (since I am calling
animate() all together). The
currentTime will start counting up for each, even though they don’t all start moving at once since
endDelay. Each has a duration of 8000ms, and each also has a delay/endDelay combination that adds up to 8000ms. Therefore, the five animations all will have a
currrentTime that maxes out at 16000ms.
I’m using a simple unstyled HTML range input in this example to act as the scrubber with a min value of
0 and a max value of
16000 to match our min/max
We need three things now:
- An animation of the range slider thumb when the animation is playing to keep in sync with the
- An event listener for when a user is dragging the slider to pause and set the
- An event listener to play the animation again when the user is done dragging
When the animation is playing we can use
requestAnimationFrame to move the range slider according to the
currentTime of the animations.
When the user is scrubbing through the full animation we can pause our individual animations and set their
currentTimes based on the range value:
Finally we can play everything again when we stop dragging and release the mouse/touch:
This has an additional check for when we stop dragging at the max value (16000). In that case we actually don’t want it to play again, but we do want it to “finish” the animations and get them to their full end states. To keep
requestAnimationFrame from firing forever once finished, we add an additional
onfinish callback to one of the animations that toggle the
isPlaying flag to false.
The Wrap Up
One limitation of the example to note: The way I’ve structured the event listeners (a combination of
change events) means it will work with touch/mouse… but not the arrow keys on a keyboard. I will be investigating better approaches for that to make this more accessible.
This scrubbing process requires a certain setup where all the animations start at the same time and have the same length. Going beyond this is possible with the current tools, but it requires a bit more thought and customization. Similar processes could be used to adjust the timeline based on scroll or other inputs.
We will have more options as the Web Animations API expands. The second level of the spec is exploring grouping and sequencing of animations such that there are multiple animations going along a shared timeline. I love that we can already accomplish a lot though with the default individual timelines and