Saturday 14 August 2010
mozRequestAnimationFrame
In Firefox 4 we've added support for two major standards for declarative animation --- SVG Animation (aka SMIL) and CSS Transitions. However, I also feel strongly that the Web needs better support for JS-based animations. No matter how rich we make declarative animations, sometimes you'll still need to write JS code to compute ("sample") the state of each animation frame. Furthermore there's a lot of JS animation code already on the Web, and it would be nice to improve its performance and smoothness without requiring authors to rewrite it into a declarative form.
Obviously you can implement animations in JS today using setTimeout/setInterval to trigger animation samples and calling Date.now() to track animation progress. There are two big problems with that approach. The biggest problem is that there is no "right" timeout value to use. Ideally, the animation would be sampled exactly as often as the browser is able to repaint the screen, up to some maximum limit (e.g., the screen refresh rate). But the author has no idea what that frame rate is going to be, and of course it can even vary from moment to moment. Under some conditions (e.g. the animation is not visible), the animation should stop sampling altogether. A secondary problem is that when there are multiple animations running --- some in JS, and some declarative animations --- it's hard to keep them synchronized. For example you'd like a script to be able to start a CSS transition and a JS animation with the same duration and have agreement on the exact moment in time when the animations are deemed to have started. At each paint you'd also like to have them sampled using the same "current time".
These problems have come up from time to time on mailing lists, for example on public-webapps. A while ago I worked out an API proposal and Boris Zbarsky just implemented it; it's in Firefox 4 beta 4. Here's the API, it's really simple:
- window.mozRequestAnimationFrame(): Signals that an animation is in progress, requests that the browser schedule a repaint of the window for the next animation frame, and requests that a MozBeforePaint event be fired before that repaint.
- The browser fires a MozBeforePaint event at the window before we repaint it. The timeStamp attribute of the event is the time, in milliseconds since the epoch, deemed to be the "current time" for all animations for this repaint.
- There is also a window.mozAnimationStartTime attribute, also in milliseconds since the epoch. When a script starts an animation, this attribute indicates when that animation should be deemed to have started. This is different from Date.now() because we ensure that between any two repaints of the window, the value of window.mozAnimationStartTime is constant, so all animations started during the same frame get the same start time. CSS transitions and SMIL animations triggered during that interval also use that start time. (In beta 4 there's a bug that means we don't quite achieve that, but we'll fix it.)
That's it! Here's an example; the relevant sample code:
var start = window.mozAnimationStartTime;
function step(event) {
var progress = event.timeStamp - start;
d.style.left = Math.min(progress/10, 200) + "px";
if (progress < 2000) {
window.mozRequestAnimationFrame();
} else {
window.removeEventListener("MozBeforePaint", step, false);
}
}
window.addEventListener("MozBeforePaint", step, false);
window.mozRequestAnimationFrame();
It's not very different from the usual setTimeout/Date.now() implementation. We use window.mozAnimationStartTime and event.timeStamp instead of calling Date.now(). We call window.mozRequestAnimationFrame() instead of setTimeout(). Converting existing code should usually be easy. You could even abstract over the differences with a wrapper that calls setTimeout/Date.now if mozAnimationStartTime/mozRequestAnimationFrame are not available. Of course, we want this to become a standard so eventually such wrappers will not be necessary!
Using this API has a few advantages, even in this simple case. The author doesn't have to guess a timeout value. If the browser is overloaded the animation will degrade gracefully instead of uselessly running the step script more times than necessary. If the page is in a hidden tab, we'll be able to throttle the frame rate down to a very low value (e.g. one frame per second), saving CPU load. (This feature has not landed yet though.)
One important feature of this API is that mozRequestAnimationFrame is "one-shot". You have to call it again from your event handler if your animation is still running. An alternative would be to have a "beginAnimation"/"endAnimation" API, but that seems more complex and slightly more likely to leave animations running forever (wasting CPU time) in error situations.
This API is compatible with browser implementations that offload some declarative animations to a dedicated "compositing thread" so they can be animated even while the main thread is blocked. (Safari does this, and we're building something like it too.) If the main thread is blocked on a single event for a long time (e.g. if a MozBeforePaint handler takes a very long time to run) it's obviously impossible for JS animations to stay in sync with animations offloaded to a compositing thread. But if the main thread stays responsive, so MozBeforePaint events can be dispatched and serviced between each compositing step performed by the compositing thread, I think we can keep JS animations in sync with the offloaded animations. We need to carefully choose the animation timestamps returned by mozAnimationStartTime and event.timeStamp and dispatch MozBeforePaint events "early enough".
mozRequestAnimationFrame is an experimental API. We do not guarantee to support it forever, and I wouldn't evangelize sites to depend on it. We've implemented it so that people can experiment with it and we can collect feedback. At the same time we'll propose it as a standard (minus the moz prefix, obviously), and author feedback on our implementation will help us make a better standard.
Comments
I don't have an alternative proposition, but that's a feeling I needed to share :)
Anthony: good points...
The original name gave me the impression that this was some ill-conceived API with a design bug of only allowing one animation to go on at a time, with mozAnimationStartTime (kept constant until animation ends) being its start time.
mozAnimationFrameTime gives more hints about it monotonically increasing over time and sugesting you store a copy yourself some place for as long as you need it.
I've built a bookmarklet that allows to switch jquery animation logic from a setInterval to a MozRequestAnimationFrame based one.
It turns out that the latter always yields lower fps. Could you have a look at my code?
Check out my URL and contact me by email if you want further info.
Thank you in advance,
Louis-Rémi
It's not a bug, it's a feature. The number of times per second that your callback fires is a bad measure of performance.
I'm not so sure. Does the browser double buffer the scene today? Does it wait for vsync already, too? In any case, how does the browser "know" I'm done painting my scene and it is safe to flip? My understanding was that most browsers just progressively draw as changes come to the DOM. I was hopeful that this event was a new way for me to cue firefox that I wanted it to flip buffers, but apparently it's more of a timing accuracy convenience.
I agree with you that it's Firefox's job to wait on vsync, it's the prerogative of the Mozilla developers to do that. So it follows that the fact that I see tearing tells me that there is no wait-on-vsync before the frame buffers flip on my system.
Still, I would argue that IF mozRequestAnimationFrame fired just after vsync, there would be a valuable benefit to my code - I would be given the maximum amount of time to make changes to the DOM and cause paints to occur before the *NEXT* vsync event. Lining up my changes with vsync would give me the best shot at getting all my DOM changes done and laid out before the browser paints the next frame. I believe this would lead to a more steady frame rate, especially on less complex scenes where I make only a few changes.
I hope that makes sense, and I hope you agree lining up vsync with MozBeforePaint could still be useful.
Could you answer these questions:
- Does Firefox wait on vsync before flipping the frame buffers? Do I need to enable some experimental D3D mode or something to get that? - If yes, is there some way to know that Firefox is waiting on vsync? (Don't want to idle the processor in mozRequestAnimationFrame if its not going to give me the desired effect.)
- What do you think of the possibility of lining up vsync with MozBeforePaint? (Because if you say "not gonna happen" I'll give up on mozRequestAnimationFrame.)
- Perhaps I am really asking for a new API/event?? MozAfterSwapBuffers? (Oh, I think that would be more appropriate, keeps mozRequestAnimationFrame uncluttered and not tied to composition rate.)
Cheers,
Dave Woldrich
Q1. I don't know, but it definitely depends on the platform and which acceleration options are being used. Please file bugs on platforms where it's not happening.
Q2. I see what you're saying and I think you're right. We should do that.
Q3. No, I don't think we need that.