Cross Browser and Legacy Supported RequestFrameAnimation

There is an awesome new feature in HTML 5, window.requestAnimationFrame, which tells the browser that you wish to perform an animation and requests that the browser schedule a repaint of the window for the next animation frame1. This allows you to do animations more efficiently and without using setInterval or series of setTimeout function(s).

I have created a Demo that attempts to use the browser’s requestAnimationFrame side-by-side with the legacy polyfill executing.

Article updated on Oct. 16, 2014

Commenter Ian Gilman pointed out that this polyfill did not work exactly as the native one. The native one will play all animations that have been queued between animation frames, not just the latest one. He took my code, modified it to behave correctly, and put it up on github (cheers!). I have updated this article to reflect his code changes. You can get the latest code on github.

Getting ready

If your browser supports HTML 5, then the requestAnimationFrame and cancelAnimationFrame will be set by:

// most browsers have an implementation
window.requestAnimationFrame = window.requestAnimationFrame ||
		window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame ||
		window.msRequestAnimationFrame;
window.cancelAnimationFrame = window.cancelAnimationFrame ||
		window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame ||
		window.msCancelAnimationFrame;

If your browser does not support HTML 5, then the requestAnimationFrame and cancelAnimationFrame will be emulated by:

var aAnimQueue = [],
    aProcessing = [],
    iRequestId = 0,
    iIntervalId;

// create a mock requestAnimationFrame function
w.requestAnimationFrame = function(callback) {
	aAnimQueue.push([++iRequestId, callback]);

    if (!iIntervalId) {
        iIntervalId = setInterval(function() {
            if (aAnimQueue.length) {
                var time = +new Date();
                // Process all of the currently outstanding frame
                // requests, but none that get added during the
                // processing.
                // Swap the arrays so we don't have to create a new
                // array every frame.
                var temp = aProcessing;
                aProcessing = aAnimQueue;
                aAnimQueue = temp;
                while (aProcessing.length) {
                    aProcessing.shift()[1](time);
                }
            } else {
                // don't continue the interval, if unnecessary
                clearInterval(iIntervalId);
                iIntervalId = undefined;
            }
        }, 1000 / 50);  // estimating support for 50 frames per second
    }

	return iRequestId;
};

// create a mock cancelAnimationFrame function
w.cancelAnimationFrame = function(requestId) {
	// find the request ID and remove it
    var i, j;
    for (i = 0, j = aAnimQueue.length; i < j; i += 1) {
        if (aAnimQueue[i][0] === requestId) {
            aAnimQueue.splice(i, 1);
            return;
        }
    }

    // If it's not in the queue, it may be in the set we're currently
    // processing (if cancelAnimationFrame is called from within a
    // requestAnimationFrame callback).
    for (i = 0, j = aProcessing.length; i < j; i += 1) {
        if (aProcessing[i][0] === requestId) {
            aProcessing.splice(i, 1);
            return;
        }
    }
};

The complete code snippet can be downloaded on github.

How it works…

The requestAnimationFrame function takes a single argument, the callback to be invoked before the repaint, and returns a positive integer that is the requestID (this will be greater than zero, but do not make any other assumptions), which can be used to stop an animation. You can stop the animation by passing requestID to the cancelAnimationFrame function. The legacy polyfill functions have the exact same signatures, but use an interval for managing animation frames.

The legacy requestAnimationFrame appends the callback function and an internal requestID to an animation queue. The requestID is simply a number that increments each time the function is called, ensuring uniqueness. If an animation setInterval is not running, one is started, and the requestID is returned. When there are items in the queue, the setInterval callback function shifts the first element in the queue and calls the animation function, otherwise the interval is terminated for performance.

The legacy cancelAnimationFrame searches the animation queue for the animation with the matching requestID, then removes it from the queuing array. It does not worry about stopping the interval, as the interval will stop automatically, if the animation queue is empty, on its next cycle.

There’s more…

There is another related property, animationStartTime, which is supported (prefixed) by some browsers, and indicates when the animation frames began. However, (at the time of this writing) browsers implement it and the timestamp passed into the animation callback function differently. Chrome sets this property to zero and passes the difference (in ms) since the animationStartTime to the animation callback function, while FireFox sets the property to ms since the epoch and passes ms since epoch into the callback function. Until a common standard is agreed upon between the browsers, it is better to implement your own start time system (if you need it), using (new Date()).getTime() or the shorter +new Date().

References:

  1. window.requestAnimationFrame on MDN
  2. Ian Gilman’s Original Changes