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 ;-)