california app design company

Cool Tricks with Animations Using requestAnimationFrame

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 everything
var redElement = document.getElementById('red-element'),
    blueElement = document.getElementById('blue-element'),
    greenElement = document.getElementById('green-element'),
    elements = [redElement, blueElement, greenElement];

// Set the colors
redElement.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 function
function 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 through
var 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 arc
var _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 arc
raf(_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 before
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 

// Nearly the same Parabola function
var _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 animation
var 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!

is a Software Engineer at Yeti. When he isn't baking bread, or messing with Raspberry Pis, he's dreaming about a future in which humanity is governed by benevolent robotic dictators. He went to school for pre-med and linguistics at the University of Florida and is hoping one day to make his parents proud by working on something tangentially related to either subject.

Enter your email to subscribe to our newsletter and get even more delicious content delivered straight to your inbox. No spam, we pinky swear.

Newsletter
blog comments powered by Disqus
Cool Tricks with Animations Using requestAnimationFrame https://s3-us-west-1.amazonaws.com/yeti-site-media/uploads/blog/.thumbnails/robot-board.jpg/robot-board-360x0.jpg
Yeti (415) 766-4198 https://s3-us-west-1.amazonaws.com/yeti-site-static/img/yeti-head-blue.png