Javascript Animation: Tutorial, Part 3

Creating Motion Tweens in Javascript

This is the long-overdue final piece of a series on creating animations in Javascript; see part 1 and part 2 for reference.

In the Flash world, objects can be animated or morphed between one state and the next over a period of time. This is where the term tween comes in. The developer does not have to write code to make the animation sequence, they simply specify the start and end points and Flash dynamically generates the frames in-between to make a smooth transition between states. With a tiny bit of math, you can easily make a reasonably-smooth tween generator in Javascript. This example will be very basic, but you could get much fancier with bezier and quadratic curves etc.

Full-featured animation APIs provided by the Yahoo! User Interface library, jQuery, Mootools and others allow you to specify start and end points, animation duration, tween effects and so on. In this article, I'll show a simple animation manager script which uses a few basic tween styles and a fixed frame count.

Less talk, and more demo:

A Simple Motion Tween

Let's say we want to move an element from point A to point B. The tween in this case can be done in a linear fashion, or according to a curve or other function. A linear motion is easy, but doesn't look as nice as a fluid motion which mimics real-life physics. A simple way to do this is a "slow-fast-slow" curve; for example, where an object starts from 0 (not moving), accelerates to a peak and then slows down to 0 again.

Using a fixed number of frames (and thus, a non-fixed duration due to the asynchronous nature of Javascript), writing a linear tween is very simple:

var points = {
  // moving a box "from" and "to", eg. on the X coordinate
  'from': 200,
  'to': 300
}
var frameCount = 20; // move from X to Y over 20 frames
var frames = []; // array of coordinates we'll compute

// "dumb" tween: "move X pixels every frame"
var tweenAmount = (points.to - points.from)/frameCount;
for (var i=0; i<frameCount; i++) {
  // calculate the points to animate
  frames[i] = points.from+(tweenAmount*i); 
}
// frames[] now looks like [205,210,215,220, ... ,300]; etc.

A more realistic tweening effect (including acceleration and deceleration) is achieved by changing the amount of motion made with each frame.

var points = {
  // moving a box "from" and "to", eg. on the X coordinate
  'from': 200,
  'to': 300
}
var animDelta = (points.to - points.from); // how far to move

// animation curve: "sum of numbers" (=100%), slow-fast-slow
var tweenAmount = [1,2,3,4,5,6,7,8,9,10,9,8,7,6,5,4,3,2,1];
// move from X to Y over frames as defined by tween curve
var frameCount = tweenAmount.length;
var frames = []; // array of coordinates we'll compute
var newFrame = points.from; // starting coordinate
for (var i=0; i<frameCount; i++) {
  // calculate the points to animate
  newFrame += (animDelta*tweenAmount[i]/100);
  frames[i] = newFrame;
}
// frames[] now looks like [201,203,206, ... ,228,236,245, ... ,297,299,300]; etc.

A linear acceleration and deceleration motion as above produces a visual improvement over a straight, linear motion. Each loop iteration adds a percentage of the total distance to move according to the tween amount, which adds up to 100%. Full-featured Javascript animation libraries go far beyond this in using bezier curves and fancier math functions, which provide much smoother motion tweens.

An Example Animation

Here, we'll create an animation object with some parameters and event handlers, and then start it.

var oAnim = new Animation({
  from: 0,
  to: 50,
  ontween: function(value) {
    writeDebug('oAnim.ontween(): value='+value);
  },
  oncomplete: function(value) {
    writeDebug('oAnim.oncomplete()');
  }
});
oAnim.start();

Nothing is actually animated on-screen in this case, but the relevant events fire and the tween value is updated as the animation runs. This could be used to move a box from 0 to 50 pixels across the screen, or from 0% to 50% of the screen or some other value.

Writing an Animation API in Javascript

Typically I like to have a single animator object which is responsible for managing all animations, following the idea that a single setInterval is regarded as the best way to do timing for Javascript animation loops. When an animation is started, a timer is created (if not already) and the collection of objects you have registered can then be animated. Despite the single timer and loop, each animation will effectively run on its own "timeline", and if you use different tweening functions, at its own pace.

For an individual animation effect from X to Y, an animation object is created with from: and to: properties, along with ontween() (called "per-frame") and oncomplete() event handlers/callbacks for the animation. The actual animation work is done within ontween in this example; most actual Javascript animation libraries allow you specify multiple HTML attributes to animate without any ontween() work needed, eg. {marginLeft:{from:100,to:300},marginTop:{from:0,to:50}} and so on. The syntax just shown is used by the animation component of the Yahoo! User Interface library.

function Animator() {
  // controller for animation objects.
  var self = this;
  var intervalRate = 20;
  this.tweenTypes = {
    // % of total distance to move per-frame, total always = 100
    'default': [1,2,3,4,5,6,7,8,9,10,9,8,7,6,5,4,3,2,1],
    'blast': [12,12,11,10,10,9,8,7,6,5,4,3,2,1],
    'linear': [10,10,10,10,10,10,10,10,10,10]
  }
  this.queue = [];
  this.queueHash = [];
  this.active = false;
  this.timer = null;
  this.createTween = function(start,end,type) {
    // return array of tween coordinate data (start->end)
    type = type||'default';
    var tween = [start];
    var tmp = start;
    var diff = end-start;
    var x = self.tweenTypes[type].length;
    for (var i=0; i<x; i++) {
      tmp += diff*self.tweenTypes[type][i]*0.01;
      tween[i] = {};
      tween[i].data = tmp;
      tween[i].event = null;
    }
    return tween;
  };

  this.enqueue = function(o,fMethod,fOnComplete) {
    // add object and associated methods to animation queue
    
    // writeDebug('animator.enqueue()');
    if (!fMethod) {
      // writeDebug('animator.enqueue(): missing fMethod');
    }
    self.queue.push(o);
    o.active = true;
  };

  this.animate = function() {
    // interval-driven loop: process queue, stop if done
    var active = 0;
    for (var i=0,j=self.queue.length; i<j; i++) {
      if (self.queue[i].active) {
        self.queue[i].animate();
        active++;
      }
    }
    if (active == 0 && self.timer) {
      // all animations finished
      
      // writeDebug('Animations complete');
      self.stop();
    } else {
      // writeDebug(active+' active');
    }
  };

  this.start = function() {
    if (self.timer || self.active) {
      // writeDebug('animator.start(): already active');
      return false;
    }
    // writeDebug('animator.start()');

    // report only if started
    self.active = true;
    self.timer = setInterval(self.animate,intervalRate);
  };

  this.stop = function() {
    // writeDebug('animator.stop()',true);

    // reset some things, clear for next batch of animations
    clearInterval(self.timer);
    self.timer = null;
    self.active = false;
    self.queue = [];
  };

};

var animator = new Animator();

Efficiency: Single Timer, Many Animations

The Animation object is a simple manager for multiple animations. It has a method for creating tweens, queueing for individual Animation() instances, and start and stop methods. When an animation is created and played for the first time, it adds itself via animator.enqueue() and then calls animator.start() if needed to kick things off. This way, the animator only ever has one single setInterval timer active - a good thing - and it only runs when animations are actively running. Once the last animation has finished, the animator shuts down until the next time it's needed.

function Animation(oParams) {
  // Individual animation sequence
  
  /*
    oParams = {
      from: 200,
      to: 300,
      tweenType: 'default', // see animator.tweenTypes (optional)
      ontween: function(value) { ... }, // method called each time (required)
      oncomplete: function() { ... } // when finished (optional)
    }
  */
  var self = this;
  if (typeof oParams.tweenType == 'undefined') {
    oParams.tweenType = 'default';
  }
  this.ontween = (oParams.ontween||null);
  this.oncomplete = (oParams.oncomplete||null);
  this.tween = animator.createTween(oParams.from,oParams.to,oParams.tweenType);
  this.frameCount = animator.tweenTypes[oParams.tweenType].length;
  this.frame = 0;
  this.active = false;

  this.animate = function() {
    // generic animation method
    if (self.active) {
      if (self.ontween && self.tween[self.frame]) {
        self.ontween(self.tween[self.frame].data);
      }
      if (self.frame++ >= self.frameCount-1) {
        // writeDebug('animation(): end');
        self.active = false;
        self.frame = 0;
        if (self.oncomplete) {
          self.oncomplete();
        }
        return false;
      }
      return true;
    } else {
      return false;
    }
  };

  this.start = function() {
    // add this to the main animation queue
    animator.enqueue(self,self.animate,self.oncomplete);
    if (!animator.active) {
      animator.start();
    }
  };

  this.stop = function() {
    self.active = false;
  };
  
};

Fun Stuff: Animation Sequences

Let's say you wanted a multi-part animation sequence, where a box resizes and moves in separate motions. A chain can easily be made using the oncomplete() method of separate animation objects.

In this case, a sequence of animations[] objects is made and a function simply iterates through them, increasing a counter each time an animation's oncomplete() fires. Animations can be fired and run simultaneously, as shown in the case of the last two.

var animations = [];
var animationCount = 0;

function nextAnimation() {
  if (animations[animationCount]) {
    // writeDebug('starting animation '+animationCount);
    animations[animationCount].start();
    animationCount++;
  }
}

// generic tween handlers
function animateBoxX(value) {
  document.getElementById('box2').style.left = value+'px';
}

function animateBoxH(value) {
  document.getElementById('box2').style.height = value+'px';
}

// motion #1
animations.push(new Animation({
  from: 0,
  to: 200,
  ontween: animateBoxX,
  oncomplete: nextAnimation
}));

// motion #2
animations.push(new Animation({
  from: parseInt(document.getElementById('box2').offsetHeight),
  to: 300,
  ontween: animateBoxH,
  oncomplete: nextAnimation
}));

// motion #3
animations.push(new Animation({
  from: 200,
  to: 400,
  ontween: animateBoxX,
  oncomplete: function() {
    // run last two animations simultaneously
    nextAnimation();
    nextAnimation();
  }
}));

// simultaneous #1
animations.push(new Animation({
  from: 400,
  to: 800,
  ontween: animateBoxX
}));

// simultaneous #2
animations.push(new Animation({
  from: 300,
  to: 600,
  ontween: animateBoxH
}));

nextAnimation(); // start the sequence

Javascript Animation API demo: Sequential + Simultaneous animation sequence

Demo Limitations, Annoyances etc.

While intended as examples, some shortcuts were taken in writing the demo animation code.

  • Fixed frame count means animation duration is variable, and varies between browsers/platforms and CPU etc. (Ideally, duration is configurable and animation runs at best-possible speed, skipping frames if need be - eg., try to render 100 fps, calculate tween array of (frames * duration) up-front and skip to nearest frame based on Date() vs. time animation started while running.)
  • One tween per animation, code doesn't conveniently handle animation of arbitrary (or multiple) HTML properties.
  • ontween() is somewhat limited, doesn't provide access to all properties of the animation object etc.

If for some reason you needed to write your own animation script, these would be items to consider. I also recommend the YUI animation library, which is quite solid. Bernie's Better Animation Class is a similar standalone script worth checking out.

Related links