JavaScript Animations

Posted in: javascript
JavaScript animations are a key aspect of dynamic Web Sites and Application development. Moreover, most JavaScript Frameworks or Libraries provide APIs for dealing with at least three main things: When developing Web Sites most JavaScript effects involve rendered DOM Elements, but sometimes JavaScript animations are used in other contexts, like when using the Canvas. In the JavaScript InfoVis Toolkit the main target of my animations are Graphs, and in the next version also Nodes and Edges as separate entities. Today I'd like to describe how to create a generic animation class that can be used or extended for any purpose. I'll try to be minimalistic and to present only the needed code for making animations. Then you might find useful to add some code to perform specific animation tasks targeting for example specific style properties of a DOM Element.

Defining an Animation Class

Before creating an Animation class we might want to consider what to expose as options to the user. The options I thought of are: Additionally we'd like to add a couple of controllers, one when a step of the animation is executed and one when the animation has completed:
var options = {
  duration: 1000,
  fps: 40,
  onStep: function(delta) {},
  onComplete: function() {}
};
delta gives us an idea of the progress of the animation. When the animation starts delta will be equal to zero. When the animation ends it'll be equal to one. We will also need a start method to trigger the animation and a step method that will compute delta at each step. Now that we defined our options we can start thinking about our implementation.

Implementing the Animation class

Our Animation class will be a class constructor that sets all the options and properties that we defined before and a prototype with the methods start and step. The class could be used like this:
var fx = new Effect({
  duration: 1000,
  fps: 40,
  onStep: function(delta) {
    /* do stuff */
  },
  onComplete: function() {
    alert('done!');
  }
});
//start the animation
fx.start();
Here's the code I came up with, inspired by the MooTools Framework:
//define the class constructor
function Effect(opt) {
  this.opt = {
    duration: opt.duration || 1000,
    fps: opt.fps || 40,
    onStep: opt.onStep || function(){},
    onComplete: opt.onComplete || function(){}
  };
}

Effect.prototype = {
  //define how the animation starts
  start: function() {
    //return if we're currently performing an animation
    if(this.timer) return;
    //trigger the animation
    var that = this, fps = this.opt.fps;
    this.time = +new Date;
    this.timer = setInterval(function() { that.step(); }, 
                             Math.round(1000/fps));
  },
  //triggered at each interval step
  step: function() {
    var currentTime = +new Date, time = this.time, opt = this.opt;
   //check if the time interval already exceeds the duration 
   if(currentTime < time + opt.duration) {
     //if not, calculate our animation progress
      var delta = (currentTime - time) / opt.duration;
      opt.onStep(delta);
    } else {
      //we already exceeded the duration, stop the effect
      //and call the onComplete callback
      this.timer = clearInterval(this.timer);
      opt.onStep(1);
      opt.onComplete();
    }
  }
};
One very common operation to do with delta is to change the interval [0, 1] of delta to our desired from and to values that we want to compute for our element. A clever thing to do would be to declare this method as a class method for Effect. We'll call it compute:
Effect.compute = function(from, to, delta) {
  return from + (to - from) * delta;
};
Now If we wanted for example to animate an element's width style from 0 to 10px we could do:
var elem = document.getElementById("myElementId"),
    style = elem.style;
var fx = new Effect({
  duration: 500,
  onStep: function(delta) {
    style.width = Effect.compute(0, 10, delta) + 'px';
  }
});
fx.start();

Extending the Effect class

The animation code defined above could be extended in different ways. For example, this class could be slightly modified to accept a DOM element in its constructor and modify style properties of that element when performing an animation. The code could look like this:
var elem = document.getElementById("myElementId");
var fx = new Effect({
  element: elem,
  duration: 1000
});
fx.start({
  'width': [0, 20, 'px'],
  'height': [0, 5, 'em']
});
The code would now look more or less like this:
//define the class constructor
function Effect(opt) {
  this.opt = {
    element: opt.element,
    duration: opt.duration || 1000,
    fps: opt.fps || 40,
    onComplete: opt.onComplete || function(){}
  };
}

Effect.prototype = {
  //props contains a hash with style properties
  start: function(props) {
    if(this.timer) return;
    var that = this, fps = this.opt.fps;
    this.time = +new Date;
    this.timer = setInterval(function() { that.step(props); }, 
                             Math.round(1000/fps));
  },
  //triggered at each interval step
  step: function(props) {
     var currentTime = +new Date, time = this.time, opt = this.opt;
     if(currentTime < time + opt.duration) {
      var delta = (currentTime - time) / opt.duration;
     //set the element style properties
     this.setProps(opt.element, props, delta);
    } else {
      this.timer = clearInterval(this.timer);
      this.setProps(opt.element, props, 1);
      opt.onComplete();
    }
  },
  //set style properties. Properties must be
  //in camelcase format.
  setProps: function(elem, props, delta) {
    var style = elem.style;
    for(var prop in props) {
      var values = props[prop];
      style[prop] = Effect.compute(values[0], values[1], delta)
        + (values[2] || '');
    }
  }
};
Other extensions might involve normalizing style keywords, adding effect transitions, adding pause resume methods, and/or using more OO JS idioms when coding these classes. I hope you got to know a little bit more about animation internals and please if you have any advice on this code, which as I told before is just for demonstration, I'll be pleased to hear you!
Comments
blog comments powered by Disqus