requestAnimationFrame sample - the right way

Some time ago, there has been a big PR ongoing by the people of the Web Performance WG to make sure we used requestAnimationFrame in our websites instead of setTimeout when it was available.

However, the latest drafts (which were implemented in IE10) introduced an undetectable breaking change from the currently published working draft, which can break your existing websites using the requestAnimationFrame function.

Worse, IE10 also has a subtle bug in animationStartTime that makes it unreliable in some cases. As a consequence, it’s now very difficult to write a code that works reliably in every browser and keep the animations in sync without browser-detection.

Meanwhile, I finally found a way to work that out in a simple way which :

  • Syncs the animations on the same frame as if you used animationStartTime
  • Works correctly whether the browser follows the old or the new specification
  • Polyfills requestAnimationFrame efficiently and correctly in older browsers

Here’s the code

// helper to create javascript animations the right way
function JSAnim(computeFrame,animationDuration) {

    this.startTime=0;
    this.animationDuration = 0;
    this.computeFrame_internal = computeFrame;

    if(!animationDuration || animationDuration<=0) {
        
        // unbound animation, indicates time as the number of ms since launch
        this.init=function(time) {
            this.startTime=time-50/3;
            this.computeFrame(time);
        }.bind(this);
        
        this.computeFrame = function(time) {
            var relTime = (time-this.startTime);
            this.computeFrame_internal(relTime);
            requestAnimationFrame(this.computeFrame);
        }.bind(this);
        
    } else {
        
        this.animationDuration = animationDuration;
        
        // bound animation, indicates time as a completion percentage (0.0 to 1.0)
        this.init=function(time) {
            this.startTime=time-50/3;
            this.computeFrame(time);
        }.bind(this);
        
        this.computeFrame = function(time) {
            var relTime = (time-this.startTime)/(this.animationDuration);
            if(relTime >= 1) {
                this.computeFrame_internal(1);
            } else {
                this.computeFrame_internal(relTime);
                requestAnimationFrame(this.computeFrame);
            }
        }.bind(this);
        
    }
    
    requestAnimationFrame(this.init);
}

// sample
new JSAnim(function(relTime) {
    document.documentElement.style.background=(
        "rgb("+
            Math.round(76*relTime)+","+
            Math.round(255*relTime)+","+
            Math.round(0*relTime)+
        ")"
    );
}, 500);

To polyfill requestAnimationFrame in an efficient way, I propose:

// start by polyfilling the requestAnimationFrame function, if needed
if(!window.requestAnimationFrame) {
    
    (function() {
        
        // this function polyfills the requestAnimationFrame back-end
        var animateTimeout = 0;
        var animateArray = [];
        var animateArray2 = []; // use only two arrays (GC optimization)
        
        var animate = function() {

            // keep anims in sync by using one time per rAF
            var fcAnimationStartTime = new Date().getTime();
            var funcs = animateArray;
            
            // make the back-end ready for a new wave of rAF calls
            animateArray = animateArray2;
            animateArray2 = funcs;
            animateTimeout = 0;
            
            // iterate over callbacks
            var callback; while (callback = funcs.pop()) {
                try {
                    callback(fcAnimationStartTime);
                } catch (ex) {
                    setTimeout(function() {
                        throw ex;
                    },0);
                }
            }
            
        };
        
        // this function polyfills requestAnimationFrame
        window.requestAnimationFrame = function(f) {
            
            // add the function to the list of callbacks
            animateArray.push(f);
            
            // ask for a callback if it's the first function
            if(animateTimeout===0) { 
                animateTimeout = setTimeout(animate, 16);
            }
            
        }
    })();
    
}

You can view an online demo here.

Feedback?

If you have any feedback, don’t hesitate to post a comment ;-)

Published on 2012-08-21 under ALL, JS, WEB

Comments

A javascript file is loading the comments.