BernieCode logo... Because I like butterflies.Home > Animator.js

UPDATE! Development progress for Animator.js:

--------------------------

In this article I describe Animator, a class for creating animated effects à la moofx or scriptaculous.

Right-click here to download Animator.js version 1.1.9, released under the BSD license (i.e. use it however you like).

Background and motivation

(You can skip this if you want to go straight to the scripts...)

I was putting together my new home page the other day, and decided that the chicken could use some lipstick in the form of one of those new-fangled animated accordion widgets. I checked several different libraries and found them all to be lacking.

In particular, they don't seem to realise that inheritance is evil, and must be destroyed. By providing base classes for an effect and requiring users to subclass it to make new effects, they create a proliferation of classes and make it too hard to create new effects that the library designer hasn't thought of (scriptaculous gets round this by thinking of every effect you might want, which is why it is so large). Have a look at this code sample from moofx, which defines 2 effect classes to animate an element's height and width:

// I make this 645 characters of code just to encapsulate the difference between
// "width" and "height" A ratio of 43 parts cruft to 1 part functionality!
fx.Height = Class.create();
Object.extend(Object.extend(fx.Height.prototype, fx.Layout.prototype), {
    increase: function() {
        this.el.style.height = this.now + "px";
    },

    toggle: function() {
        if (this.el.offsetHeight > 0) this.custom(this.el.offsetHeight, 0);
        else this.custom(0, this.el.scrollHeight);
    }
});

fx.Width = Class.create();
Object.extend(Object.extend(fx.Width.prototype, fx.Layout.prototype), {
    increase: function() {
        this.el.style.width = this.now + "px";
    },

    toggle: function(){
        if (this.el.offsetWidth > 0) this.custom(this.el.offsetWidth, 0);
        else this.custom(0, this.iniWidth);
    }
});

Disclaimer: I use moofx as an example of bad library design only because I love its functionality so much. Lest I be accused of thinking that I'm somehow better than its author Valerio Proietti, I should point out that (a) he had the idea first, and (b) he has a much better blog photo than me.

Don't get me wrong, inheritance in JavaScript has its uses, but extending the behaviour of effect objects isn't one of them. The Strategy design pattern is much better.

I wrote this class in order to:

  1. make an easy to use, well featured animation class with the smallest file size possible.
  2. apply some proper OOAD to JavaScript animation. In particular, Animator uses composition (several classes interact to provide complex behaviour) and parameterisation (classes accept arguments that alter their behaviour) instead of inheritance.
  3. reinvent the wheel, because I'm a pedant :o)

A better way of animating

Check this out:

// This object controls the progress of the animation
ex1 = new Animator()
// the Animator's subjects define its behaviour
ex1.addSubject(updateButton);
function updateButton(value) {
    $('ex1Button').innerHTML = "Progress: " + Math.round(value * 100) + "%";
}
// now click below: each click to the button calls ex1.toggle()

The Animator above was left with the default options, here's an example that uses more configuration:

ex2 = new Animator({
    duration: 1200,
    interval: 400,
    onComplete: function() {$('ex2Target').innerHTML += "Bing! ";}
})
ex2.addSubject(updateButton);
function updateButton(value) {
    $('ex2Target').innerHTML += " Badda ";
}

Animating element styles

Most of the time you want to animate one or more a style properties of an element. There are essentially only three kinds of CSS value - ones that scale numerically (like 10px), ones that scale with a RGB colour value(like #RRGGBB), and ones that don't scale (like bold / italic). Animator provides three utility classes for each of these kinds of properties, and between them they can animate any CSS style.

// animate margin-left from 0 to 100 px
ex3 = new Animator()
    .addSubject(new NumericalStyleSubject($('ex3Button'), 'margin-left', 0, 100));
// animate background-color from white to black
ex4 = new Animator()
    .addSubject(new ColorStyleSubject($('ex4Button'), 'background-color', "#FFFFFF", "#F4C"));
// animating both - note how calls to addSubject() can be chained together:
ex5 = new Animator()
    .addSubject(new NumericalStyleSubject($('ex5Button'), 'margin-left', 0, 100))
    .addSubject(new ColorStyleSubject($('ex5Button'), 'background-color', "#FFFFFF", "#F4C"))
    .addSubject(new DiscreteStyleSubject($('ex5Button'), 'font-weight', "normal", "bold", 0.5));
// also, check out the last line, which causes font-weight to switch from normal to bold half way

If you've ever used moofx or scriptaculous then you are probably thinking that this is quite verbose, and you'd be right. Animator has a killer feature that removes the verbosity, but before we get to that, here are a couple more things you can do:

More complex effects

What if you have a number of elements that you want to animate in the same way? In that case, pass an array of elements into the Subject's constructor. There is no way to add and remove elements from a Subject after it has been constructed - if you want to do that, use one Subject for each element and use addSubject() and removeSubject() on the Animator.

// Applying the same effect to different elements is easy
ex6 = new Animator().addSubject(new NumericalStyleSubject(
    [$('dot1'), $('dot2'), $('dot3')], 'margin-left', 10, 50));
     
// applying different effects to different elements is possible
ex7 = new Animator()
    .addSubject(new ColorStyleSubject($("ex5ButtonA"), 'background-color', "#F99", "#9F9"))
    .addSubject(new ColorStyleSubject($("ex5ButtonB"), 'background-color', "#9F9", "#F99"));
   

Opacity gets special treatment. Since IE does not support the standard 'opacity' CSS style, NumericalStyleSubject will convert it into an appropriate filter.

ex8 = new Animator().addSubject(new NumericalStyleSubject(
    $("ex8Button"), 'opacity', 1, 0.25));

Controlling the animation

When you click on a samply button in this article, you are calling toggle() on an animator object. There are a few more control functions:

play()
0%
reverse()
seekTo(0.5)
seekFromTo(0.25, 0.75)

The benefit of using seekTo() is that it wil avoid sudden jumps in state when called half way through an animation:

this div uses play() and reverse on mouseover and mouseout
this div uses seekTo(1) and seekTo(0) on mouseover and mouseout

Transitions

A transition is a function that takes a state (a number between 0 and 1) and returns another number between 0 and 1. You can pass in a transition to an Animator object's constructor using the cleverly named transition property.

The Animator.tx object provides a few ready made transitions:

Sometimes you'll want to fine tune the above transitions. If you look at the source code where the Animator.tx object is created, you'll see that the above functions are all made by four factory functions:

Animator.makeEaseIn()

// make a transition that gradually accelerates. pass in 1 for smooth
// gravitational acceleration, higher values for an exaggerated effect
ex9 = new Animator({
    transition: Animator.makeEaseIn(3),
    duration: 1000
});
ex9.addSubject(new NumericalStyleSubject(
    $("ex9Button"), 'margin-left', 0, 200));

Animator.makeElastic()

// make a transition that, like an object with momentum being attracted
// to a target point, goes past the target then returns

ex10 = new Animator({
    transition: Animator.makeElastic(3),
    duration: 2000
});
ex10.addSubject(new NumericalStyleSubject(
    $("ex10Button"), 'margin-left', 0, 200));

Animator.makeBounce()

// make a transition that, like a ball falling to floor, reaches
// the target and bounces back again
ex11 = new Animator({
    transition: Animator.makeBounce(3),
    duration: 2000
});
ex11.addSubject(new NumericalStyleSubject(
    $("ex11Button"), 'margin-left', 0, 200));

Animator.makeADSR()

An Attack Decay Sustain Release envelope is a technique I lifted from music production. It is very useful for animations that start and end at the same value.

// This example shows you what an ADSR envelope looks like, but is otherwise useless
// the first three arguments are the boundary points of the 4 phases. The last is the
// sustain level. All should be between 0 and 1.
ex12 = new Animator({
    transition: Animator.makeADSR(0.25, 0.5, 0.75, 0.5),
    duration: 2000
});
ex12.addSubject(new NumericalStyleSubject(
    $("ex12Button"), 'margin-left', 0, 400));

A practical use of ADSR is to hold an animation in a certain state for a while, as in this yellow fade example

// This yellow fade is emphasised by holding it at full yellow
// for the first half of the animation
ex13= new Animator({
    transition: Animator.makeADSR(0, 0, 0.5, 1),
    duration: 1500
});
ex13.addSubject(new ColorStyleSubject(
    $("ex13Button"), 'background-color', "#FFFFFF", "#FFFF00"));

Custom functions

Of course, you can write your own functions that do any kind of transition:

// return a function that provides a configurable number of wobbles 
function wobbleFactory() {
    var wobbles = parseInt(prompt("Enter a number of wobbles (try between 1 and 5)", ""));
    if (!wobbles) {
        alert("Sorry, I didn't understand that, have 2 wobbles.");
        wobbles = 2;
    }
    return function(pos) {
        return ((-Math.cos(pos*Math.PI*((1+(2*wobbles))*pos))/2) + 0.5);
    }
}
ex14 = new Animator({
    transition: wobbleFactory(),
    duration: 2000
});
ex14.addSubject(new NumericalStyleSubject(
    $("ex14Button"), 'margin-left', 0, 100));
 

The killer feature

Like I said before, it's all a bit verbose at the moment, and most of the code in the above examples is just boilerplate. What we need is some kind of language that lets us define the style that we want to animate an object towards. Oh wait a second, we've already got one: CSS. A CSS style contains all the information we need to define an animation state:

ex15 = new Animator().addSubject(new CSSStyleSubject(
    $('ex15Button'),
    "width: 10em; background-color: rgb(256, 256, 256); font-style: normal",
    "width: 40em; background-color: #F39; font-style: italic"));
// note how you can use any unit, not just 'px'.

CSSStyleSubject is a wrapper around the other three Subjects. It parses two CSS rule sets and for each property declaration, creates a NumericalStyleSubject if it looks like a number, or a ColorStyleSubject if it looks like a colour, or a DiscreteStyleSubject otherwise. DiscreteStyleSubjects are created with a threshold of 0.5, in other words the style changes from normal to italic half way through the animation.

Conveniently, you can also pass in CSS class names instead of rule sets:

ex16 = new Animator().addSubject(new CSSStyleSubject(
    $('ex16Button'), "small white", "big blue bendy"));
// the classes small, big, white, blue and bendy are defined in this page's source.

When you're creating animations from CSS classes, it's easy to lose track of what your animator object is doing. The Animator.inspect() method returns a string that describes the animator:

This is pretty good, but we can still remove some more cruft. Most of the time, the element you want to animate will already be in its initial style. If this is the case, you can omit one of the rule sets and the initial state will be inferred from the elements current style. This uses getComputedStyle (or Element.currentStyle in IE) so reflects the element's actual style after applying CSS rules in style sheets.

ex17 = new Animator().addSubject(new CSSStyleSubject(
    $('ex17Button'), "width: 300px; background-color: #F39"));

Finally, there is one last bit of syntactic sugar to make it easy to apply effects. The Animator.apply(element, style, options) function is a wrapper around creating a single CSSStyleSubject. The second argument is the style to fade to and the third is an optional set of constructor parameters for the Animator object. If you want to specify the full from and to styles, pass in a two item array as the second parameter.

ex18 = Animator.apply($('ex18Button'), "greenish"); // ta da!

Oh go on, one more feature...

By popular demand... Several people asked for an easy way to chain several animations together. AnimatorChainSubject is a subject that accepts an array of other animator objects and from them creates a new animation that plays each in turn.

var animators = [];
for (var i=0; i<3; i++) {
    animators[i] = Animator.apply($("ex18blob-" + i), "blobEnd");
}
ex19 = new AnimatorChain(animators);
// the AnimatorChain object has toggle(), play(), reverse() and seekTo(state) functions
// just like Animator objects, so you can often use them where code expects an Animator