Understanding animation, duration and easing using requestAnimationFrame
Creating performant animations with requestAnimationFrame
It’s the greatest invention since the stroopwafel: requestAnimationFrame
. It’s the cornerstone of your favourite animation library, and it’s a great way to create your own animations. In canvas
, for example. But how does it work? Weirdly enough, I see a lot of front-end developers run faster from requestAnimationFrame
documentation than they do from someone coughing on the train these days. And I get it. It’s scary. It’s about functions calling themselves in some sort of loop, timestamps, performance? Let’s find out.
What is animation?
In order to understand how to work with requestAnimationFrame
, we’re going to have to understand motion first. The human brain can’t really see objects move. When we look at a bouncing ball, the average human eye behaves like a photo camera and takes about one hundred and fifty (150) little snapshots per second and sends those images to the brain. The brain perceives this as motion, because every snapshot is a little further in time and thus the movement has progressed a little more. In animation, these snapshots are called frames.
Creating frames for animation
Let’s say we want to animate a red square from left to right. In order to do so, we’re going to need to create those frames we’ve talked about. Because we need to move the red square in each frame a little more to the right in order to create motion, we’re going to have to execute a function for each frame.
“One hundred and fifty frames per second? Excuse me?” — Haha yeah, I know. That’s a lot of function executions per second. Fortunately, we don’t need to perfectly mimic the human eye to achieve smooth animation. Opinions differ around this topic, though. Some say 24 frames per second (fps) is enough. I say: “screw that”. We should always aim for 60 fps.
“Sixty frames per second? Excuse me?” — Still probably you right now. But don’t worry, my performance hunting friend.
Getting optimal performance with requestAnimationFrame
In order to get to those optimal 60 frames per second (fps), we could use a setInterval
and run that function sixty times per second. The problem is that if the user’s computer is too busy running your useless WebGL website, it doesn’t have enough resources to execute those sixty function calls per second but it will still try to do so.
Understanding why this isn’t going to work and what’s happening behind the scenes of the browser in the event loop is beyond the scope of this article, but just know: your computer slows down, animations don’t get executed the way they should and everything just goes to shit.
In 2018, Jake Archibald gave an amazing talk on the event loop. It’s a must-watch if you’re interested in how requestAnimationFrame works behind the scenes
Thankfully, the mighty performance gods have given us requestAnimationFrame
, a method available on the window
object. When you call it, it’ll execute a callback function whenever it has enough resources to do a repaint of everything that’s on your current screen. You’re telling the browser: “Please queue this task, and execute when you’re ready to repaint”.
But this only gets executed once. When we animate something, we want to get those 60 executions per second, to achieve animations with a decent amount of frames per second. But how do we do this?
First of all, we need to create some sort of loop to continuously request more frames.
And here is where we first harness the power of requestAnimationFrame
. In the animate
function, we’re essentially creating a new frame by giving the square a new position. But in order to get smooth animation, this should happen sixty times per second. We do this by calling the animate
function from itself with the result that it keeps repeating itself. When the browser has the resources, it will render another frame with what’s happening in the animate function. And because we’re calling the animate
function from itself, it will go in an infinite loop until we tell it to stop.
But how does requestAnimationFrame get to 60(?) FPS?
Because you assign your animate
function as a callback to your requestAnimationFrame
it only gets called when requestAnimationFrame
sees an opportunity to repaint the browser window. Per the spec, it will try to do so for a maximum amount that is synced with the refresh rate of your monitor (Hz). Most monitors are 60Hz, that’s why requestAnimationFrame
will try to run sixty times per second (60fps). With a 120Hz monitor it’ll try to run 120 times per second (120fps).
I emphasised maximum there, because it’s not going to render more frames per second than the monitor is capable of showing.
But like we said before, requestAnimationFrame
only fires when there are enough resources to do a repaint. What if you’re running those custom written GLSL shaders in your code, that makes your computer hotter than Henry Cavill with a moustache? That would mean you’re low on CPU resources and it might be possible that requestAnimationFrame
executes your callback only 30 times per second.
Apparently there’s no way to know how many frames requestAnimationFrame
is going to give us. But if one user has 30 frames and the other has 120 frames per second, and you would animate five pixels per frame, it would mean that every user would see a different animation. Well, the smart specification people thought about that as well.
Timing your animations with requestAnimationFrame
Since the amount of frames is arbitrary, let’s forget about them for now and work with what we do know: time.
The requestAnimationFrame
passes one argument to your callback. It’s a timestamp with how much milliseconds have passed since the document loaded. Based on that timestamp we’re going to calculate what our new left
(translateX
) value should be based on the time that has passed, and let requestAnimationFrame
worry about the frames. You can read that again if you want. Slowly. Out loud. It’s complex. But you can handle it.
Let’s write this up in code. Don’t worry, we’ll go through it step by step. First up: calculate the relative time progression, also known as normalisation of the value:
Now that we have our relative progression (a float between 0.0 and 1.0) we can calculate what our new translateX
value should be:
In each frame we’re calculating the new left position based on time passed since our animation started. In this way it doesn’t matter how many frames the user gets. More frames per second means less pixels to animate per frame. Less frames per second means more pixels to animate per frame.
Now to the last part of this article. I promise. Really.
How to use easing in requestAnimationFrame
Easing is like when the animation first goes fast, then a little slower and then fast again right? How does that work? Well, it’s math. And I’m not a math teacher. In fact, I absolutely suck at math. But maybe we can simplify things a bit.
We talked about relative progression earlier. We were calculating a value between 0.0 and 1.0 based on the time passed. If we were to plot that into a graph, it would be pretty linear:
But what happens if we would cheat our animation? That we would tell it 0.4 seconds have passed, when actually it was 0.2 seconds. And after that tell it 0.6 seconds have passed, when it was actually 0.8 seconds that had passed. The graph would like this:
This is what easing functions do. They tell the animation to go to a certain point in time that’s off the linear path.
If we want to implement that in our example it wouldn’t be much of a hassle, because we’ve got our relative progression already. I’m going to use the easeInSine easing. We can use it in our javascript by using the Bezier Easing NPM package:
Yeah, I know. This is the shitty life of a front-end developer. Writing 30 lines of code to move a red square a couple of pixels to the right. But congratulations! You made it ’till the end! Thank you for reading, and hopefully you became a little bit smarter.
If you have any questions, don’t hesitate to leave a comment or DM me at https://twitter.com/trekinbami.
XOXO.