requestAnimationFrame for smart(er) animating
Tuesday, December 20, 2011 8:42:16 PM
Quite a while back Paul Irish wrote a good blog post entitled "requestAnimationFrame for smart animating" where he introduced the new API and also presented a shim that you could start using that would be future proof. Implementations are now catching up with the shim, but there are still a few lacking it and considering the longevity of old IE versions there will be for quite some time. It didn't take long until I realized the shim is easily (mis)used causing it to drift.
A simple example. Let's say you are using requestAnimFrame (the shim) to drive an animation that you're expecting to run at 60FPS. That gives you 16.7ms to draw each frame and looking at the shim that's exactly the timeout set if it's not implemented in the browser (1000 / 60). If you call that at the start of your frame and then do your rendering that's fine. But what happens if you by chance call requestAnimFrame AFTER you've done your rendering? Say for example that you're using up 15ms to render each frame. Then when it's time to draw a frame you'd first render for 15ms and then wait for 16.7ms before drawing the next frame. Each frame would then take 31.7ms and you'd only be able to get an FPS of 31 as compared to 60 if it's natively implemented.
I've seen this one time too many to let it slip. I should've done this the first time I was asked to check out some sample running slower in Opera, but procrastination is for cool kids.
EDIT: Thanks to Joel Fillmore for spotting that the time parameter to the callback was missing. While editing this I figured I might as well update it to include the cancelRequestAnimationFrame. I'm yet to see it used in a real world sample, but for completeness.
It's certainly a little bit wordier than the original shim, and perhaps a bit trickier to read, but it does its best effort to account for drift and you can call it wherever you want in your update loop.
The built in timer is of course never quite high rez enough when you do stuff like this, not to mention what it's like when you're running it on battery power on a laptop, but that's a whole different story.
A simple example. Let's say you are using requestAnimFrame (the shim) to drive an animation that you're expecting to run at 60FPS. That gives you 16.7ms to draw each frame and looking at the shim that's exactly the timeout set if it's not implemented in the browser (1000 / 60). If you call that at the start of your frame and then do your rendering that's fine. But what happens if you by chance call requestAnimFrame AFTER you've done your rendering? Say for example that you're using up 15ms to render each frame. Then when it's time to draw a frame you'd first render for 15ms and then wait for 16.7ms before drawing the next frame. Each frame would then take 31.7ms and you'd only be able to get an FPS of 31 as compared to 60 if it's natively implemented.
I've seen this one time too many to let it slip. I should've done this the first time I was asked to check out some sample running slower in Opera, but procrastination is for cool kids.
EDIT: Thanks to Joel Fillmore for spotting that the time parameter to the callback was missing. While editing this I figured I might as well update it to include the cancelRequestAnimationFrame. I'm yet to see it used in a real world sample, but for completeness.
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelRequestAnimationFrame = window[vendors[x]+
'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}())
It's certainly a little bit wordier than the original shim, and perhaps a bit trickier to read, but it does its best effort to account for drift and you can call it wherever you want in your update loop.
The built in timer is of course never quite high rez enough when you do stuff like this, not to mention what it's like when you're running it on battery power on a laptop, but that's a whole different story.

