yeti logo icon
Close Icon
contact us
Yeti postage stamp
We'll reply within 24 hours.
Thank you! Your message has been received!
A yeti hand giving a thumb's up
Oops! Something went wrong while submitting the form.

Cool Tricks with Animations Using requestAnimationFrame

By
-
May 22, 2017

If you've been following my never-ending adventures with light sensors (see here and here and here), you'll see that I have yet to make any cool things subscribe to my Websocket server.

This post will be less about having the browser communicate with Websockets, and even less about light sensors, and more about some tricks that helped me when using the low(er)-level animation API requestAnimationFrame as I was writing a web-browser subscriber for a Proof-of-Concept.

What is requestAnimationFrame?

This function is built-in to many modern browsers and is an API that enables developers to script animations by essentially "painting" the desired styles one frame at a time, and then calling itself to paint the next frame. requestAnimationFrame takes one argument, simply a callback that has two main characteristics:

  1. takes an argument that is an ultra-accurate timestamp of when it would be called
  2. must call window.requestAnimationFrame(<itself>) to paint the next frame

Another thing to keep in mind is that animations with requestAnimationFrame are non-blocking, which means if you make subsequent calls to requestAnimationFrame, the resulting animations will all occur at the same time!

Quick and dirty example

This example of the recommended use comes straight from the MDN Page

var start = null;var element = document.getElementById('SomeElementYouWantToAnimate');element.style.position = 'absolute';function step(timestamp) {  if (!start) start = timestamp;  var progress = timestamp - start;  element.style.left = Math.min(progress / 10, 200) + 'px';  if (progress < 2000) {    window.requestAnimationFrame(step);  }}window.requestAnimationFrame(step);

To summarize, this animation moves an absolute element to the right (by increasing the left style) by at most 200 pixels within 2 seconds. The animation will only call itself and produce the next "frame" if the timestamp is within 2000 milliseconds of the start time. Pretty simple!

Trick 1: Using a function returning functions as your step function

In the example above, the function performs fine with just the timestamp as its argument. But what if you want more things fed to your function? Take the example above as an example (teehee). The step function will only work on a globally defined element object. What if we want to call this animation on a bunch of different elements?

Enter the function that returns a function:

// Initialize everythingvar redElement = document.getElementById('red-element'),    blueElement = document.getElementById('blue-element'),    greenElement = document.getElementById('green-element'),    elements = [redElement, blueElement, greenElement];// Set the colorsredElement.style.backgroundColor = 'red';blueElement.style.backgroundColor = 'blue';greenElement.style.backgroundColor = 'green';// Loop through and make all the elements absolute// (P.S. doing this because `position` was set in the example above)// (P.P.S. I'd rather this be defined in the stylesheets though)for (var idx = 0; idx < elements.length; idx++) {    elements[idx].style.position = 'absolute';}// Modified step function that returns another functionfunction step(start, element) {  return function (timestamp) {    if (!start) start = timestamp;    var progress = timestamp - start;    element.style.left = Math.min(progress / 10, 200) + 'px';    if (progress < 2000) {      // remember to feed in the argument for the subsequent calls      window.requestAnimationFrame(step(start, element));     }  }}// This interval animates each element one-at-a-time, every 2 seconds // until they've all been run throughvar animationInterval = setInterval(function () {    var start = null;    if (elements.length > 0) {        // Take the first element out...        var firstElement = elements.shift();        // ...and animate it        window.requestAnimationFrame(step(start, firstElement));      } else {        clearInterval(animationInterval);    }}, 2500);

Above, we now have three elements of three different colors all using the same step function within requestAnimationFrame. Here's the result:

Magical Animated Dots Animation

Trick 2: Not using the timestamp argument at all

Now having a statically defined duration time to your animations is both lovely and manageable, but what if you want your animations to go from point A to point B without thinking too hard in advance about the time it should take? A good example is animating according to the light sensor data being fed in through websocket. If my light sensor says "0" at one moment and "45" in the next, what would I define as the ideal duration of that animation? What about "54" to "23"? Sure, I could have another function take some sort of desired max time (say the desired time it would take for my animation to go from 0% light to 100% light) and then use the difference between two numbers and this max value to calculate a desired progress time, OR I can throw all that jazz out the window and let the API worry about how long it will take for me. Here's what I mean:

var width = 50, // Width of the container (in vw)    stepSize = 0.01, // Size of each step,    sunElement = document.getElementById('sun'),    raf = window.requestAnimationFrame; // Aliased requestAnimationFrame function// Step method that gradually drags // an element from one point to another on a// parabolic arcvar _parabolaStep = function (  el,  startProg,  endProg,  asc = null // Necessary to know which direction we're going in) {    return function () {        // Decide whether this function is ascending or descending in progress        if (asc === null) asc = startProg - endProg < 0;        // Step in the right direction        var newProg = asc ? startProg + stepSize : startProg - stepSize;         // "getParabolaXY" takes the width and progress percentage and         // returns an array with [X, Y] that percentage corresponds to.        var newXY = getParabolaXY(width, newProg);        el.style.left = `${newXY[0]}vw`;        el.style.bottom = `${newXY[1]}vh`;        if (asc ? newProg < endProg : newProg > endProg)             // "paint" the next frame            raf(_parabolaStep(el, newProg, endProg, asc));         }    };}// Goes from 40% of the arc to 20% of the arcraf(_parabolaStep(sunElement, 0.4, 0.2)); setTimeout(function () {    // Goes from 20% of the arc up to 100% of the arc, after 2 seconds    raf(_parabolaStep(sunElement, 0.2, 1)); }, 2000)

The code above illustrates how we can still do animations without directly setting a desired duration. If I want this animation to go faster, I could increase the step size and inversely, also expect the animation to go slower if I decrease the step size. There is one remaining issue though. Remember how I said having a "defined duration time to your animations is both lovely and manageable"? In the example above, I had to set a timeout for 2 seconds, just so that one animation doesn't cut into the other one. That's not ideal, especially since the time it takes to go from point A to point B can vary, depending on how far apart point A and point B are in the progression. What could we use that is a Javascript construct that is fully customize-able and can enable us to make things wait to occur until we want them to (Hint: it's a Promise)?

Trick 3: Promise-ifying non-timestamped animations

Rather than a bare call to requestAnimationFrame, we can wrap animations we want blocked in a Promise that resolves once we hit whatever we decide the "end" is. Let's take our parabola animation method and make it a Promise.

// Same variables as beforevar width = 50, // Width of the container (in vw)    stepSize = 0.01, // Size of each step,    sunElement = document.getElementById('sun'),      raf = window.requestAnimationFrame; // Aliased requestAnimationFrame function // Nearly the same Parabola functionvar _parabolaStep = function (  el,  startProg,  endProg,  asc = null // Necessary to know which direction we're going in,  resolve // Now, feed in pointer to the resolve method of our Promise) {    return function () {        // Decide whether this function is ascending or descending in progress        if (asc === null) asc = startProg - endProg < 0;        // Step in the right direction         var newProg = asc ? startProg + stepSize : startProg - stepSize;        // "getParabolaXY" takes the width and progress percentage and         // returns an array with [X, Y] that percentage corresponds to.        var newXY = getParabolaXY(width, newProg);        el.style.left = `${newXY[0]}vw`;        el.style.bottom = `${newXY[1]}vh`;        if (asc ? newProg < endProg : newProg > endProg) {            // "painting" the next frame            raf(_parabolaStep(el, newProg, endProg, asc, resolve));         } else {            // Call the hook to the resolve Promise once it's clear the animation is complete            resolve();         }    };}// Function that creates a Promise wrapper for our animationvar makeParabolaStep = function (el, from, to) {    return new Promise(function (resolve) {        raf(_parabolaStep(el, from, to, resolve));    });};// No more need for `setTimeout`, just chain with `.then()`makeParabolaStep(sunElement, 0.1, 0.9).then(function () {    // Will not run until after `sunElement` has gone from `10%` to `90%`    return makeParabolaStep(sunElement, 0.9, 0.45);}).then(function () {    // Will not run until after `sunElement` has gone from `90%` to `45%`    return makeParabolaStep(sunElement, 0.45, 0.4);});

parabola now with house

Wrap-Up

To sum it up, this post shows a few things:

  1. requestAnimationFrame is a powerful API for handcrafting some very customized animations
  2. Writing functions that return functions are useful for subverting an API (whether or not that's best practice is up to the context).
  3. It is possible to write animations with requestAnimationFrame without using the timestamp argument in the callback at all.
  4. Animations sans timestamps can be chained together within Promises.

Happy building!

You Might also like...

colorful swirlsAn Introduction to Neural Networks

Join James McNamara in this insightful talk as he navigates the intricate world of neural networks, deep learning, and artificial intelligence. From the evolution of architectures like CNNs and RNNs to groundbreaking techniques like word embeddings and transformers, discover the transformative impact of AI in image recognition, natural language processing, and even coding assistance.

A keyboardThe Symbolicon: My Journey to an Ineffective 10-key Keyboard

Join developer Jonny in exploring the Symbolicon, a unique 10-key custom keyboard inspired by the Braille alphabet. Delve into the conceptualization, ideas, and the hands-on process of building this unique keyboard!

Cross-Domain Product Analytics with PostHog

Insightful product analytics can provide a treasure trove of valuable user information. Unfortunately, there are also a large number of roadblocks to obtaining accurate user data. In this article we explore PostHog and how it can help you improve your cross-domain user analytics.

Browse all Blog Articles

Ready for your new product adventure?

Let's Get Started