Gain Motion Superpowers with requestAnimationFrame

Benjamin De Cock
7 min readAug 28, 2017

--

In my last blog post explaining some of the technical details behind Connect, I briefly touched on the different options we now have to animate things on the web. I prioritized these options by ease of use, ending consequently with requestAnimationFrame:

The sky is the limit, but you have to engineer the rocket ship. The possibilities are endless and the rendering methods unlimited (HTML, SVG, canvas — you name it), but it’s a lot more complicated to use and may not perform as well as the previous options.

The seeming complexity of requestAnimationFrame unfortunately scares many designers away: as a low-level API, it doesn’t provide much by default, resulting in boilerplate code that might initially seem disproportionate for the most basic effects. But once you grasp the few fundamental concepts, you enter a whole new world of possibilities. Let’s demystify this fantastic animation tool!

Frames, Time and Progress

At its core, requestAnimationFrame doesn’t do much: it’s basically just a method that executes a callback. In fact, there are very few differences between doing requestAnimationFrame(doSomething) and doSomething(). So, what’s so special about it? I’m glad you asked! In short:

  • requestAnimationFrame schedules the callback call on the next repaint
  • requestAnimationFrame passes the callback the current time

There are a few other distinctions, but these are the main benefits. Now, requestAnimationFrame doesn’t create an animation on its own, it’s the sequence of successive callbacks that will make things move on the screen. Let’s see how it works by creating our first animation loop:

That’s it. We start the loop by calling requestAnimationFrame(tick) and we keep calling it recursively. The loop doesn’t do anything so far, but it’s working: if you log the now argument inside the tick function, you’ll see the time steadily increasing. And since we haven’t set any kinds of limit, we’ve effectively created an infinite loop, which is rarely what we want.

In order to fix that, we’ll simply track the elapsed time and stop the loop once we reach an arbitrary duration. We already have the current time given by the now argument, so we can just store it on the first call and compare it with the current time on every frame to calculate the elapsed time:

Our animation loop now stops after two seconds as intended, but we can clean up the code a little bit. The high resolution timer that gives the now argument its precise value isn’t exclusive to requestAnimationFrame: the Performance interface also allows us to fetch the same value, giving us a cleaner way to store the start time:

The animation loop still doesn’t do anything, but we now have the foundation to build our animation on. By comparing the elapsed time with the total duration, we can determine the progress of the animation on every frame. And with this progress (represented by a number between 0 and 1), it’s now straightforward to compute our style updates.

And voilà! You can preview the result of this code in the following CodePen:

If you open the JS pane on the demo above, you’ll notice the progress is calculated a little bit more precisely by using Math.min(elapsed / total, 1). The reason for this is that the result of the division will likely never give a perfectly round 1.00, which would result in a slightly inaccurate final position. By clamping the progress this way, we ensure the animation will end on the exact value we specified.

Easing

The progress in our previous example was calculated linearly, which produces a constant translation speed. A linear animation curve is one of the worst offenders for any sane motion designer, so our top priority is to replace that with proper easing!

While calculating a linear progress is trivial, the math for creating a nice acceleration isn’t—at least for me. Luckily enough, Robert Penner (❤) and other smart people have written many easing equations for the rest of us. For example, this function creates a nice ease-out curve:

Its usage is dead simple: pass it the linear progress we already calculated, and get a lovely decelerating progress in return! Thanks to the easing, the previous demo now looks a lot less offensive:

These easings being just mathematical equations, you can do a lot more than with declarative animations, such as creating elastic curves:

With these concepts alone—tracking the time, determining the progress, and applying an easing—you’ve enough tools in your tool belt to build pretty much any animation you want, no matter the rendering method.

Scalable Vector Graphics

While the previous examples relied on HTML and CSS, requestAnimationFrame is render-agnostic and can therefore also manipulate other interfaces like SVG. In fact, this is where requestAnimationFrame truly shines: with the fine control it provides you and the ginormous API surface area SVG exposes, your animation options are virtually infinite. Visual effects like morphing and interpolation now become possible using the same underlying principles. Consider the following example:

The play button turns into a stop icon by gradually moving each point of the SVG shape to its final position. You can toggle the tabs above to inspect the entire code but, basically, this is what it comes down to:

That’s the beauty of animating SVG with requestAnimationFrame: since you can manipulate any attribute through the DOM, you can animate literally anything that takes a numerical value. For example, SVG has a stdDeviation attribute which—as opposed to CSS blurs—lets you control the direction of the blur. Combine that with requestAnimationFrame and you get the chance to simulate a delightful motion blur:

And since you’re responsible for determining the progress of your animation, you’re not even limited to predefined timelines anymore as you can dynamically compute the next step at any point of the animation. For instance, it’s up to you to move something along an arbitrary path instead of following a straight trajectory by adjusting the coordinates on every frame:

The versatility of requestAnimationFrame makes it a powerful tool for all sorts of animations and, guess what? It can do even more than that!

Timers

As we discussed earlier, requestAnimationFrame is basically just a (great) timer at its core. It’s generally used to schedule animations, but nothing prevents us to use its capabilities for different purposes.

In my animation work, I often try to incorporate a decent amount of randomness in the timings and effects in order to create something more interesting and “organic”. For example, the rocket SVG icon used on Connect randomizes the frequency at which the stars appear, creating a much more convincing landscape.

This random sequence relies on requestAnimationFrame: the timer calculates a random duration within a certain range, executes a callback and starts the process again. Here’s how it works:

These subtle variations can bring a lot to an animation, and requestAnimationFrame is perfectly suited for this task: it is performant, consistent, precise and battery-efficient, which leads us to the next topic.

Performance and Framerate

As we briefly saw earlier, requestAnimationFrame claims a new frame to execute a callback. requestAnimationFrame intelligently determines the appropriate framerate based on the screen’s refresh rate and the device’s capabilities. Usually, this means your callback will fire 60 times per second, giving you approximately 16 milliseconds to complete a frame. If the work you do in your callback takes less than that, your animation will run at 60FPS. In most cases, this frame budget will be vastly larger than what you need, even for expensive tasks. Here’s a lot of moving stuff as an example:

Unlike CSS animations, requestAnimationFrame doesn’t run on a separate thread. In theory, that means animations might stutter under heavy CPU load. In practice, the most common circumstance for jank is when HTTP requests are happening during your animation. So, if you plan for example to animate a loading spinner in JavaScript, use the Web Animations API instead. Otherwise, go nuts!

Advanced Performance

We just looked into user-perceived performance, but the Performance interface is just as fascinating! We introduced performance.now() earlier as another way to fetch the timestamp, but the Performance interface has more to offer and can help us abstract our time tracking code to make it even more reusable. Specifically, we’ll take advantage of the following methods:

  • performance.mark(): stores a timestamp in the browser’s entry buffer
  • performance.getEntriesByName(): returns a list of performance entries

We’ll offload the time tracking logic to a standalone function which will be responsible to deal with the Performance interface. On the first call, it’ll create a new entry using the requestAnimationFrame identifier, conveniently storing the start time for us. On subsequent calls, it’ll compare this mark with the current timestamp to return the elapsed time. Finally, it’ll clear the entry to avoid filling up the browser’s buffer.

Our tick() function is now considerably simpler: we don’t need to remember to pass it a timestamp argument anymore, save the start time of the animation, calculate the elapsed time, etc. We simply get the progress back, which is all we actually care about. Additionally, it becomes easier to create new animation helpers, such as a better setTimeout:

These functions increase a little bit your boilerplate code, but greatly alleviate your animation loops, which is where your focus should be. They’re gathered in this boilerplate code file alongside some other niceties in order to get you started quickly.

While verbose and arguably spooky at first sight, requestAnimationFrame is a wonderful tool for motion designers. Spring physics, WebGL animations, Creative Coding—there’s no bound in your creativity. By providing you just a set of basic building blocks, you get the freedom to explore new ideas you wouldn’t even have considered before. So, if you’re serious about animations, I highly recommend you to take the plunge. Enjoy!

--

--