Fully dynamic animations
This article is a part of my Web Animations article series.
By now we used static animations itself or in a dynamic context. Let us dive into the techniques of coding fully dynamic animations. In contrast to static animation we will not use any kind of help from CSS Animation or CSS Transition.
The basic pattern
We can simply create an animation by defining a simple rendering loop:
const animateNextFrame = () => {
// change an element's style here
// ...
// continue rendering at next frame
requestAnimationFrame(animateNextFrame)
}
// start the animation
requestAnimationFrame(animateNextFrame)
Inside the animateNextFrame
function we can modify our elements we want to animate. At the end we tell the browser that he should call animateNextFrame
again when he wants to render the next frame.
The animation starts when we first request to call the animateNextFrame
function.
I made an example with a box moving one pixel to the right:
Frames and time
When we are doing fully dynamic animations we need be aware that we are computing frames. The number of frames computed per second is differing between devices.
Background: The browser usually uses the display’s refresh rate as the upper limit to define how many frames it renders per seconds. A typical display has a 60 Hz refresh rate and high-end displays up to 120–240 Hz. The frame rate is also influenced by the CPU and GPU load, the animation complexity and the device’s energy settings. That means, we can’t assume a certain frame rate.
We can solve this by using the progress of current animation as the primary unit to specify our animation. It’s an approved method that is also used by CSS Animation to define the keyframe timings.
Do you remember? It’s a value between 0.0
(0%, start of animation) and 1.0
(100%, end of animation). This unit has several advantages:
- It’s time agnostic. We later multiply it with any animation duration and can dynamically speed up / slow down our animation.
- It’s perfect for applying timing functions. Applying a non-linear timing function is very easy.
- It’s a perfect computation base. Many algorithms works flawlessly with a normalized, relative value between
0.0
and1.0
.
Compute the progress value
Computing the progress value is simple. We just need to remember when our animation started. For each frame we compute how many time passed. Then we use modulo to “step in the current animation iteration”. Then we just divide by the animation duration.
Here’s an example computation:
timePassed = 3500
animationDuration = 2000
timePassedThisIteration = timePassed % animationDuration
= 3500 % 2000
= 1500
progress = timePassedThisIteration / animationDuration
= 1500 / 2000
= 0.75
Or just progress = (passedTime % duration) / duration
Apply the progress value
Let us apply the progress value-based modelling approach to our moving box. First we need to specify how fast the box should move. Let’s say it should move the full distance of 200px in 500ms. We derive from it:
- At progress
0
the box shouldn’t be moved. - At progress
1
the box should be 200px rightwards. - The animation duration is
500ms
Let’s start by doing some time measurement. The browser has a function performance.now()
that returns the number of milliseconds passed after page load. We remember this number once at the animation start. Then we can compute the progress each frame:
const startOfAnimation = performance.now()
const duration = 500
const animateNextFrame = () => {
const timePassed = performance.now() - startOfAnimation
const progress = (timePassed % duration) / duration
requestAnimationFrame(animateNextFrame)
}
requestAnimationFrame(animateNextFrame)
The rest is simple math. We multiply the progress value with our 200px for each frame to get a frame rate agnostic position of our box:
const boxPositionX = progress * 200
The box will take the same time to move 200px on every device. A 120 Hz displays we will just compute more intermediate frames.
Have a look:
Animation configuration
We can apply the same pattern of animation configuration we used with our dynamically chained CSS Transitions to control the animation. We simply add a configuration object that can be modified from outside and used as parameter in the animation computation function.
You may notice that our animation directly reacts to our configuration change. That’s fine for now. I will discuss techniques to fix this in another article.