Skip to content

Instantly share code, notes, and snippets.

@remy
Last active January 12, 2024 11:57
Show Gist options
  • Save remy/36f388d72c1ef161582f to your computer and use it in GitHub Desktop.
Save remy/36f388d72c1ef161582f to your computer and use it in GitHub Desktop.
requestAnimationFrame helper

raf.js

A simple script with a few niceties that allows for multiple requestAnimationFrame calls, and FPS pinning.

How it works

The script polyfills rAF if required, then overloads requestAnimationFrame and cancelAnimationFrame with a process that allows multiple frames to be queued up for rAF to run.

This is useful if there are multiple animations running on the page, you want all the callbacks to happen at once, and not on multiple rAF calls. This script is meant as a drop-in solution to that problem.

API

By default it will overload the original requestAnimationFrame methods, but if you remove the two arguments at the end of the script (or change them) it will introduce window.raf (or what you've named the request & cancel functions).

  • requestAnimationFrame(fn): can be called multiple times at once, and the queue will only be cleared when the real rAF callback fires
  • requestAnimationFrame.cancel: helper to cancelAnimationFrame - returns true if a handler was due to fire.
  • requestAnimationFrame.fps(fn, fps): helper to pin you function to run every N frames per second. fps is not milliseconds. Note that this is more akin to setInterval than rAF, as it will reschedule your function to run every N frames.
  • requestAnimationFrame.running: boolean flag to pause all rAF calls - set to false and animations will stop, set to true and they'll resume.
(function(window, request, cancel) {
'use strict';
var lastTime = 0;
var strings = {
raf: 'requestAnimationFrame',
caf: 'cancelAnimationFrame',
af: 'AnimationFrame',
};
/**
* requestAnimationFrame shim layer with setTimeout fallback
* @see http://paulirish.com/2011/requestanimationframe-for-smart-animating
* modified for use with raf shim and raf based setInterval/Timeout
*/
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window[strings.raf]; ++x) {
window[strings.raf] = window[vendors[x]+'Request' + strings.af];
window[strings.caf] =
window[vendors[x]+'Cancel' + strings.af] || window[vendors[x]+'CancelRequest' + strings.af];
}
if (!window[strings.raf]) {
window[strings.raf] = function(callback) {
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[strings.caf]) {
window[strings.caf] = function(id) {
window.clearTimeout(id);
};
}
/* ^---------- end of polyfill, start of raf shim ------------> */
var queue = [];
var lookup = {};
var guid = 0;
var _rAF = window[strings.raf];
var _cAF = window[strings.caf];
function clearQueue(time) {
_rAF(clearQueue);
if (!queue.length || !raf.running) {
return;
}
var todo = queue.splice(0); // move items across
var i = 0;
var item = null;
var length = todo.length;
for (; i < length; i++) {
item = todo[i];
if (!item.cancel) {
item.fn(time);
}
delete lookup[item.guid];
}
}
clearQueue();
var raf = function raf(fn) {
guid++;
lookup[guid] = fn;
queue.push({
guid: guid,
cancel: false,
fn: fn,
});
return guid;
};
raf.running = true;
raf.cancel = function (id) {
if (lookup[id]) {
lookup[id].cancel = true;
return true;
}
return false;
};
raf.fps = function (fn, fps) {
var lastFrameTime = 0;
var ms = 1000 / fps;
function update(elapsedTime) {
// calculate the delta since the last frame
var delta = elapsedTime - (lastFrameTime || 0);
// queue up an rAF update call
raf(update);
// if we *don't* already have a first frame, and the
// delta is less than 33ms (30fps in this case) then
// don't do anything and return
if (lastFrameTime && delta < ms) {
return;
}
// else we have a frame we want to update at our fps...
// capture the last frame update time so we can work out
// a delta next time.
lastFrameTime = elapsedTime;
// now do the frame update and render work
fn(elapsedTime);
}
update();
};
if (cancel) {
window[cancel] = raf.cancel;
}
if (!request) {
request = 'raf';
}
window[request] = raf;
})(window, 'requestAnimationFrame', 'cancelAnimationFrame');
!function(a,b,c){"use strict";function m(a){if(k(m),h.length&&n.running)for(var b=h.splice(0),c=0,d=null,e=b.length;e>c;c++)d=b[c],d.cancel||d.fn(a),delete i[d.guid]}for(var d=0,e={raf:"requestAnimationFrame",caf:"cancelAnimationFrame",af:"AnimationFrame"},f=["ms","moz","webkit","o"],g=0;g<f.length&&!a[e.raf];++g)a[e.raf]=a[f[g]+"Request"+e.af],a[e.caf]=a[f[g]+"Cancel"+e.af]||a[f[g]+"CancelRequest"+e.af];a[e.raf]||(a[e.raf]=function(b){var c=(new Date).getTime(),e=Math.max(0,16-(c-d)),f=a.setTimeout(function(){b(c+e)},e);return d=c+e,f}),a[e.caf]||(a[e.caf]=function(b){a.clearTimeout(b)});var h=[],i={},j=0,k=a[e.raf];a[e.caf],m();var n=function(a){return j++,i[j]=a,h.push({guid:j,cancel:!1,fn:a}),j};n.running=!0,n.cancel=function(a){return i[a]?(i[a].cancel=!0,!0):!1},n.fps=function(a,b){function e(b){var f=b-(c||0);n(e),c&&d>f||(c=b,a(b))}var c=0,d=1e3/b;e()},c&&(a[c]=n.cancel),b||(b="raf"),a[b]=n}(window,"requestAnimationFrame","cancelAnimationFrame");
@ermik
Copy link

ermik commented Apr 29, 2018

Wow! Great.

@cesarpachon
Copy link

hello @remy! I am trying to understand the rationale of this script.. as far as I had read, the original rAF requests are actually queued until the browser is ready for render, and then all the callbacks executed at once (well, on sequence, that is the same that you do when clean the queue).. so, at a first glance looks like there is no difference with the original behavior. I appreciate your explanation.

@volodalexey
Copy link

Is this a workaround for this question ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment