Simple Animations Using requestAnimationFrame
Animating DOM elements involves modifying a CSS style every few milliseconds to give the illusion of movement. This means passing in a callback function to setTimeout
and modifying the node’s style
object within that callback. Then calling setTimeout
again to queue the next animation frame.
From the ashes of the phoenix rises a new helper function to write animations called requestAnimationFrame
. It started off in Firefox 4 and is slowly being adopted by all browsers including IE 10. And luckily it is easy to make it backwards compatible with older browsers.
window.requestAnimationFrame(callbackFunction);
Unlike setTimeout
, which runs after a specified time delay, requestAnimationFrame
runs a callback the next time the browser is going to paint the screen. This allows you to synchronize with the paint cycles of the browser, so that you won’t be painting too often or not often enough, which means your animations will be silky smooth, yet not too demanding on your CPU.
Sifting through browser inconsistencies
Currently every browser has a prefixed version of requestAnimationFrame
so lets feature detect which version is supported and make a reference to it:
var _requestAnimationFrame = function(win, t) {
return win["webkitR" + t] || win["r" + t] || win["mozR" + t]
|| win["msR" + t] || function(fn) { setTimeout(fn, 60) }
}(window, "equestAnimationFrame");
Notice how we’re using the bracket notation to access a property on the window
object. We’re using the bracket notation because we’re building the property name on the fly using string concatenation. And if the browser doesn’t support it, we’re falling back to a regular function that calls setTimeout
after 60 milliseconds to achieve a similar effect.
Building the shell
Now let’s build a simple function that will call our _requestAnimationFrame
repeatedly to mimic the animation.
To achieve the animation, we’ll need an outer function that serves as an entry point and an inner function that will be called repeatedly, called a stepping function.
function animate() {
var step = function() {
_requestAnimationFrame(step);
}
step();
}
At every call of the stepping function, we need to keep track of the progress of the animation to know when to end. We’ll calculate when the animation is supposed to finish and base our progress on how much time is left during each cycle.
function animate() {
var duration = 1000*3, //3 seconds
end = +new Date() + duration;
var step = function() {
var current = +new Date(),
remaining = end - current;
if(remaining < 60) {
//end animation here as there's less than 60 milliseconds left
return;
} else {
var rate = 1 - remaining/duration;
//do some animation
}
_requestAnimationFrame(step);
}
step();
}
Notice we’re doing +new Date()
to get the current time in milliseconds. The plus sign coerces the date object into a numerical data type.
The rate
variable is a number between 0 and 1 that represents the progress rate of the animation.
Making it useful
Now we need to think about the function’s inputs and outputs. Let’s allow the function to accept a function and duration as parameters.
function animate(item) {
var duration = 1000*item.time,
end = +new Date() + duration;
var step = function() {
var current = +new Date(),
remaining = end - current;
if(remaining < 60) {
item.run(1); //1 = progress is at 100%
return;
} else {
var rate = 1 - remaining/duration;
item.run(rate);
}
_requestAnimationFrame(step);
}
step();
}
And we can call this function like this:
animate({
time: 3, //time in seconds
run: function(rate) { /* do something with rate */ }
});
Inside the run function I’ll put some code that animates the width of a node from “100px” to “300px”.
animate({
time: 3,
run: function(rate) {
document.getElementById("box").style
.width = (rate*(300 - 100) + 100) + "px";
}
});
Improving the use-case
It works fine, but what I really want is to be able to input an array of functions that gets called one after the other. So that after the first animation ends, the second animation picks up. We’ll treat the array as a stack, popping off items one at a time. Let’s change the inputs:
function animate(list) {
var item,
duration,
end = 0;
var step = function() {
var current = +new Date(),
remaining = end - current;
if(remaining < 60) {
if(item) item.run(1); //1 = progress is at 100%
item = list.shift(); //get the next item
if(item) {
duration = item.time*1000;
end = current + duration;
item.run(0); //0 = progress is at 0%
} else {
return;
}
} else {
var rate = remaining/duration;
rate = 1 - Math.pow(rate, 3); //easing formula
item.run(rate);
}
_requestAnimationFrame(step);
};
step();
}
When the animation is first run, item
is null and remaining
is less than 60 milliseconds, so we pop the first item off the array and start executing it. On the last frame of the animation, remaining
is also less than 60, so we finish off the current animation and pop the next item off the array and start animating the next item.
Notice also that I’ve put the rate
value through an easing formula. The value from 0 to 1 now grows with cubic proportions and makes it look less robotic.
To call the animation function we do:
animate([
{
time: 2,
run: function(rate) {
document.getElementById("box").style
.width = (rate*(300 - 100) + 100) + "px";
}
}, {
time: 2,
run: function(rate) {
document.getElementById("box").style
.height = (rate*(300 - 100) + 100) + "px";
}
}
]);
Notice how the width of the box expands first taking up 2 seconds, before the height expands which takes up another 2 seconds.
Wrapping it up
Let’s clean up our code a little. Notice how we’re calling getElementById
so many times that it’s not funny anymore? Let’s cache that and let’s cache the start and end values while we’re at it.
animate([
{
time: 2,
node: document.getElementById("box"),
start: 100,
end: 300,
run: function(rate) {
this.node.style
.width = (rate*(this.end - this.start) + this.start) + "px";
}
}
]);
Notice how we don’t need to modify the main function, because the run
function was part of a self-contained object the whole time and has access to all the properties of the object via the this
variable. Now whenever the stepping function is run, we have all variables cached up.
And there you have it. A simple animation helper that takes advantage of requestAnimationFrame
with a fallback for old browsers.