How JavaScript animation works

I often find myself looking at certain things that the major libraries do and thinking “I wonder how they do that?” Animation was one of those, so I looked through jQuery and YUI to see how they did it, built my own animation function to teach myself and now that it’s working, I’ll show everyone else how it works.

When I first looked at the idea of animation, I thought it was quite straight-forward: divide the duration by 13 (since that’s the smallest cross-browser number of milliseconds we can have between intervals) to get the ticks, divide the difference in style change by those ticks and keep adding that difference until the animation is complete. But when I tried that, I found that my animation often took longer than expected and easing was completely impossible. Clearly a new direction was needed.

It turns out that a far better method is to keep track of the time, since that’s what the easing functions generally expect. More on easing a little later.

Getting started

We’ll know what the style is going to be when the animation ends since it’s sent to the animation function. What we need to begin with is a function that can tell us what the current style of the element is. Luckily, John Resig came up with on in his book Pro JavaScript Techniques which will do the job nicely:

function getStyle(elem, name) {

    var style = null;
// If the property exists in style[] then it's been recently set and it's
// current.
    if (elem.style[name]) {
        style = elem.style[name];

// Check to see if we can use the W3C method. Be aware that we need
// hyphenated names ("text-align" rather than "textAlign").
    } else if (document.defaultView && document.defaultView.getComputedStyle) {
        name = name.replace(/([A-Z])/g, '-$1').toLowerCase();
        style = document.defaultView.getComputedStyle(elem, '');
        style = style && style.getPropertyValue(name);

// Otherwise try the IE method.
    } else if (elem.currentStyle) {
        style = elem.currentStyle[name];
    }

// Return anything we found.
    return style;
}

Now we can work out how to read the current style, it’s time to start working on the animation function itself.

The animation function itself

Well, a start at least. We need to set up a few variables to get an animation started. Not many, just 4. We also need our function to accept 5 arguments: the element, the properties that we wish to animate in key/value pairs ({height: 400, width: 1000} for example), the duration of our animation, the easing we wish to use and a callback function to be executed when the animation is complete. Here’s the start of our animation function:

function animate(elem, props, duration, easing, callback) {

// The smallest cross-browser interval that we can have is 13 milliseconds.
    var interval = 13,

// Convert all the properties that we were given into an array of style names.
        styles = Object.keys(props),

// Send each of the style names through the getStyle function so we get an
// array of the styles the element started with.
        beginningStyle = styles.map(function (s) {
            return parseInt(getStyle(elem, s), 10);
        }),

// Make a note of the current time.
        startTime = +new Date();

}

Now we get to the meat of the of the function. We need to change the element’s style by a small part of the difference and we need to do it a few times. To understand the next piece of code, I have to explain why I prefer to use a combination of setTimeout functions rather than a single setInterval.

The difference between setTimeout combinations and setInterval

Yes, you’re right, setInterval was designed for the process I’ve just described, yes it’s a standard JavaScript function and yes it works consistently across all the major browsers. But setInterval has 1 annoying feature: it fires the function that it has been passed every x milliseconds (the x being the second argument) whether the previous firing completed or not. On the other hand, putting a setTimeout function at the end of another one will mean that everything inside the function must have completed before the next one is called (or queued). Since DOM manipulation can be slow, I feel more comfortable using the combination:

function animate(elem, props, duration, easing, callback) {

    var interval = 13,
        styles = Object.keys(props),
        beginningStyle = styles.map(function (s) {
            return parseInt(getStyle(elem, s), 10);
        }),
        startTime = +new Date();

    setTimeout(function anim() {
        var repeat = true;

// Here we work out whether the function needs to repeat or not.

        if (repeat) {
            setTimeout(anim, interval);
        } else {
            goToEnd(elem, props, callback);
        }
    }, interval);

}

Because we may be waiting a few more milliseconds that we expected, we’ll work out how long we’ve actually taken to do this animation each time the inner anim function is called; that’s why we needed to know the time we started the animation. You’ll also notice that we’ve created a new goToEnd function. That function doesn’t do much, just sets the elements styles to our end points and fires the callback:

function goToEnd(elem, props, callback) {
    Object.keys(props).forEach(function (s) {
        elem.style[s] = props[s] + 'px';
    });
    if (callback) {
        callback.call(elem);
    }
}

We use that function at the end so that we guarentee that the element is the correct size when our animation finishes. It also gives us a short-cut to completing the animation if our looping takes a little too long.

So, now that we’ve got the start point figured and the end point written, I guess we’d better start adjusting the element style.

Adjusting the element style

The maths needed to create a nice, smooth animation is really quite simple. All we need to do it complete that calculation for each style every few milliseconds and we’ll have a perfectly valid animation function:

function animate(elem, props, duration, easing, callback) {

    var interval = 13,
        styles = Object.keys(props),
        beginningStyle = styles.map(function (s) {
            return parseInt(getStyle(elem, s), 10);
        }),
        startTime = +new Date();

    setTimeout(function anim() {
        var repeat = true;

        styles.forEach(function (s, i) {

// Work out how many milliseconds have passed since this function was first
// called.
            var time = +new Date() - startTime,

// Divide that time by the duration to get a number between 0 and 1 that tells
// us how far through the animation we are.
                sofar = time / duration,

// The change in the element is the new style minus the starting style.
                change = parseInt(props[s], 10) - beginningStyle[i],

// If we multiple that change by the sofar variable, we have the difference
// that should have happened by this point.
                amount = change * sofar;

// Add the difference to the beginning style and we have the style as it should
// be right now.
            amount += beginningStyle[i];

// Set that style.
            elem.style[s] = amount + 'px';

// If time is less than our duration, we'll need to do this again.
            repeat = time < duration;
        });

        if (repeat) {
            setTimeout(anim, interval);
        } else {
            goToEnd(elem, props, callback);
        }
    }, interval);

}

And there you have it, that's what an animation function looks like. Quite simple when you break it down. However, as any regular JSLinter will tell you, "Unused variable easing" so let's justify that variable.

Easing

Easing describes the way that the animation happens and the speed of the animation at any point. What we've just calculated above is usually called "linear" easing, it's a constant increase until the animation is complete. It's also really boring. What the animation function really needs is a way to set different easing by simply passing in a string. That string will look up the easing function and calculate the element's style at that moment in time.

The easing functions that are almost universally used in JavaScript started life in ActionScript and Lingo (the languages used in Adobe Flash and Director, or Macromedia as they were back then) before they were translated into JavaScript; they've been used ever since. Each easing function takes 4 main variables: the amount of time that has passed in the animation, the beginning value, the change in value and duration of the animation (typically written as "t, b, c, d.") They return the new number based on those 4. Here's an example of the linear easing re-written as an easing function and another easing method to help show how it all works:

var easingFuncs = {
    linear: function (t, b, c, d) {
        return c * t / d + b;
    },
    random: function (t, b, c, d) {
        return Math.random() * easingFuncs.linear.apply(window, arguments);
    }
};

Why anyone would want to use that random easing I'm not entirely sure but it gives you an idea of how it all works. Now we just need to tweak the animation function to use one of these functions.

function animate(elem, props, duration, easing, callback) {

    var interval = 13,
        styles = Object.keys(props),
        beginningStyle = styles.map(function (s) {
            return parseInt(getStyle(elem, s), 10);
        }),
        startTime = +new Date();

// Get the easing function or use linear if we don't recognise the method that
// the user gave us.
    easing = easingFuncs[easing] || easingFuncs.linear;

    setTimeout(function anim() {
        var repeat = true;

        styles.forEach(function (s, i) {
            var time = +new Date() - startTime,

// Pass the time, the beginning value, the change and the duration to which
// ever easing function we've got.
                amount = easing(time,
                    beginningStyle[i],
                    parseInt(props[s], 10) - beginningStyle[i],
                    duration);
            elem.style[s] = amount + 'px';
            repeat = time < duration;
        });

        if (repeat) {
            setTimeout(anim, interval);
        } else {
            goToEnd(elem, props, callback);
        }
    }, interval);

}

Now that's a better animation function. I've put this function into jsfiddle with a lot more easing methods and a shim for older browsers. Feel free to play around with it.

Room for improvement

The animation function that I've just described only really works for pixel values (height, width etc), it tends to go a little strange when you try to set things like opacity. This is because the style is set by adding 'px' to the end of the value in both the animation and goToEnd functions. If you want to fix that, you need to work out which properies require which unit and which ones require no units at all. You'll also need to take into account older versions of IE that set opacity with the command element.style.filters = 'alpha(opacity=XX)'; rather than the W3C method of element.style.opacity = 0.XX; This is far from impossible - jQuery does it by using a series of lookups for certain styles that it calls "cssHooks." If the cssHook exists, it uses the set function inside that hook rather than simply assigning the style value like we have.

You could also tweak the animation function so that it doesn't require all the variables and can work out if the user simply passed in an element, properties and a callback. Again, this is very simple, but a little beyond a post that's simply describing how animation works.

Leave a Reply

Your email address will not be published. Required fields are marked *