Award-Winning Fjords Thomas Reynolds

How Do I: Animate

This is the first piece in a series of "How Do I" articles. Since that phrase can be taken multiple ways, let me explain: this is how I, Thomas Reynolds, accompish certain tasks. This isn't about comparing multiple technique or even justifying my prefered approach. This is a brain dump.

Animation Philosophy

When animating, either massive thousand-part experiences or single on-off effects, I prioritize framerate and the ability get the right "feel" from the animation. Both goals are about Control, so I do nearly all of my animations in Javascript instead of CSS.

Javascript allows me to control all of my animations from a single place, a Tween engine, and run them from a single place, requestAnimationFrame. Using a Tween engine, instead of CSS, allows me to define very specific timing functions which influence the "feel" of the animation. Does is bounce? Move as if gravity is pulling it down? These are important. Simply defaulting to jQuery's "swing" or CSS's "ease-in-out" aren't good enough.

You can look at the recent Nike+ Fuelstream project for an example of a site using tons of Javascript animations.

tween.js

My preferred Tweening engine is tween.js, but pretty much any of them will do. jQuery's built-in, generic, $.animate function is worth a look too.

Using tween.js, you can construct animations like so:

var elem = document.getElementById('box');
var tween = new TWEEN.Tween({ height: 100 })
  .to({ height: 200 }, 1000)
  .easing(TWEEN.Easing.Elastic.InOut)
  .onUpdate(function() {
    elem.style.height = this.height + 'px';
  }).start();

You may notice, I'm using TWEEN.Easing.Elastic.InOut. Its timing curve looks like:

Elastic.InOut

You can see that it travels below and above the lines, representing 0 and 1. For a long time, this kind of timing was impossible with CSS transitions. That's changed, but some of the more mathmatically complicated easing methods still have to be done in Javascript. Here are the timing curves for all of tween.js' built-in functions.

requestAnimationFrame

Of course, if you actually ran this code, nothing would happen. That's because tween.js needs to be told "what time is it" to know which point in the animation we are at. For that, I use requestAnimationFrame.

requestAnimationFrame is a browser feature which will run our code at 60 frames per second. As a nice fallback, if it detects that the browser is too slow to run our animation at 60fps, it will automatically fallback to 30fps. Using a requestAnimationFrame loop syncs our animations so they all paint at once and at the same frequency as our displays. Simply put, this is the best possible way to run animations if you prioritize framerate.

We can run the above Tween with requestAnimationFrame like so:

function animate(timestamp) {
  if (!timestamp) {
    timestamp = +(new Date());
  }

  // Update/draw all Tweens
  TWEEN.update(timestamp);

  // Next frame
  requestAnimationFrame(animate);
}

// Start rAF
requestAnimationFrame(animate);

One caveat, with a loop like this, requestAnimationFrame will always be running and eating up CPU cycles and battery. It's a better practice to only be using it when you actually need to animate something.

One-off Animations

Unless you're doing large, sequenced animations, you probably just want a quick one-off tween. This is how most people using jQuery's animate. I've written a simple function which abstracts the Tween engine, sets up the tween and runs requestAnimationFrame for the duration of the animation, then exits.

function oneOffAnimation(from, to, duration, 
                          easing, onUpdate,
                          onComplete) {
  var t = new TWEEN.Tween(from)
    .to(to, duration)
    .easing(easing)
    .onUpdate(onUpdate)
    .onComplete(function _onComplete() {
      t.done = true;
    });

  var self = this;
  function tick(ts) {
    if (!ts) {
      ts = +(new Date());
    }

    t.update(ts);

    if (t.done) {
      onComplete();
    } else {
      requestAnimationFrame(tick);
    }
  }

  t.start();
  requestAnimationFrame(tick);

  return t;
};

Which would make the above tween look like:

var elem = document.getElementById('box');
oneOffAnimation(
  { height: 100 },
  { height: 200 },
  1000,
  TWEEN.Easing.Elastic.InOut,
  function _onUpdate() {
    elem.style.height = this.height + 'px';
  },
  function _onComplete() {
    // All done!
  }
);

CSS Animations

The only place I would use CSS transitions are for simple, reversable hover effects. Changing the color of links or adding hover states to "clickable" elements. However, given the move to touch-based devices, I'd still do many interaction animations in Javascript which allows touch and gesture detection code to respond with the correct animation. It seems Android and iOS treat :hover differently. iOS makes you touch once to activate :hover then touch again to activate click. Android seems to request holding down to activate :hover and a single touch causes click. Doing this kind of work in Javascript also keeps things consistent.

Past Problems, Future Bug Fixes