Why should I use requestAnimationFrame rather than setTimeout or setInterval?
This self-answered question is a documentation example.
Why should I use requestAnimationFrame rather than setTimeout or setInterval?
This self-answered question is a documentation example.
requestAnimationFrame
produces higher quality animation completely eliminating flicker and shear that can happen when using setTimeout
or setInterval
, and reduce or completely remove frame skips.
is when a new canvas buffer is presented to the display buffer midway through the display scan resulting in a shear line caused by the mismatched animation positions.
is caused when the canvas buffer is presented to the display buffer before the canvas has been fully rendered.
is caused when the time between rendering frames is not in precise sync with the display hardware. Every so many frames a frame will be skipped producing inconsistent animation. (There are method to reduce this but personally I think these methods produce worse overall results) As most devices use 60 frames per second (or multiple of) resulting in a new frame every 16.666...ms and the timers setTimeout
and setInterval
use integers values they can never perfectly match the framerate (rounding up to 17ms if you have interval = 1000/60
)
Update The answer to the question requestAnimationFrame loop not correct fps shows how setTimeout's frame time is inconsistent and compares it to requestAnimationFrame.
The demo shows a simple animation (stripes moving across the screen) clicking the mouse button will switch between the rendering update methods used.
There are several update methods used. It will depend on the hardware setup you are running as to what the exact appearance of the animation artifacts will be. You will be looking for little twitches in the movement of the stripes
Note. You may have display sync turned off, or hardware acceleration off which will affect the quality of all the timing methods. Low end devices may also have trouble with the animation
Timer Uses setTimeout to animate. Time is 1000/60
RAF Best Quality, Uses requestAnimationFrame to animate
Dual Timers, Uses two timers, one called every 1000/60 clears and another to render.
UPDATE OCT 2019 There have been some changes in how timers present content. To show that setInterval
does not correctly sync with the display refresh I have changed the Dual timers example to show that using more than one setInterval
can still cause serious flicker The extent of the flickering this will produce depends on hardware set up.
RAF with timed animation, Uses requestAnimationFrame but animates using frame elapsed time. This technique is very common in animations. I believe it is flawed but I leave that up to the viewer
Timer with timed animation. As "RAF with timed animation" and is used in this case to overcome frame skip seen in "Timer" method. Again I think it sucks, but the gaming community swears it is the best method to use when you don't have access to display refresh
/** SimpleFullCanvasMouse.js begin **/
var backBuff;
var bctx;
const STRIPE_WIDTH = 250;
var textWidth;
const helpText = "Click mouse to change render update method.";
var onResize = function(){
if(backBuff === undefined){
backBuff = document.createElement("canvas") ;
bctx = backBuff.getContext("2d");
}
backBuff.width = canvas.width;
backBuff.height = canvas.height;
bctx.fillStyle = "White"
bctx.fillRect(0,0,w,h);
bctx.fillStyle = "Black";
for(var i = 0; i < w; i += STRIPE_WIDTH){
bctx.fillRect(i,0,STRIPE_WIDTH/2,h) ;
}
ctx.font = "20px arial";
ctx.textAlign = "center";
ctx.font = "20px arial";
textWidth = ctx.measureText(helpText).width;
};
var tick = 0;
var displayMethod = 0;
var methods = "Timer,RAF Best Quality,Dual Timers,RAF with timed animation,Timer with timed animation".split(",");
var dualTimersActive = false;
var hdl1, hdl2
function display(timeAdvance){ // put code in here
tick += timeAdvance;
tick %= w;
ctx.drawImage(backBuff,tick-w,0);
ctx.drawImage(backBuff,tick,0);
if(textWidth !== undefined){
ctx.fillStyle = "rgba(255,255,255,0.7)";
ctx.fillRect(w /2 - textWidth/2, 0,textWidth,40);
ctx.fillStyle = "black";
ctx.fillText(helpText,w/2, 14);
ctx.fillText("Display method : " + methods[displayMethod],w/2, 34);
}
if(mouse.buttonRaw&1){
displayMethod += 1;
displayMethod %= methods.length;
mouse.buttonRaw = 0;
lastTime = null;
tick = 0;
if(dualTimersActive) {
dualTimersActive = false;
clearInterval(hdl1);
clearInterval(hdl2);
updateMethods[displayMethod]()
}
}
}
//==================================================================================================
// The following code is support code that provides me with a standard interface to various forums.
// It provides a mouse interface, a full screen canvas, and some global often used variable
// like canvas, ctx, mouse, w, h (width and height), globalTime
// This code is not intended to be part of the answer unless specified and has been formated to reduce
// display size. It should not be used as an example of how to write a canvas interface.
// By Blindman67
const U = undefined;const RESIZE_DEBOUNCE_TIME = 100;
var w,h,cw,ch,canvas,ctx,mouse,createCanvas,resizeCanvas,setGlobals,globalTime=0,resizeCount = 0;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () { var c,cs; cs = (c = document.createElement("canvas")).style; cs.position = "absolute"; cs.top = cs.left = "0px"; cs.zIndex = 1000; document.body.appendChild(c); return c;}
resizeCanvas = function () {
if (canvas === U) { canvas = createCanvas(); } canvas.width = window.innerWidth; canvas.height = window.innerHeight; ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals(); } if (typeof onResize === "function"){ resizeCount += 1; setTimeout(debounceResize,RESIZE_DEBOUNCE_TIME);}
}
function debounceResize(){ resizeCount -= 1; if(resizeCount <= 0){ onResize();}}
setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; mouse.updateBounds(); }
mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0, over : false, bm : [1, 2, 4, 6, 5, 3],
active : false,bounds : null, crashRecover : null, mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.x = e.clientX - m.bounds.left; m.y = e.clientY - m.bounds.top;
m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }
else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
else if (t === "mouseover") { m.over = true; }
else if (t === "mousewheel") { m.w = e.wheelDelta; }
else if (t === "DOMMouseScroll") { m.w = -e.detail; }
if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
if((m.buttonRaw & 2) && m.crashRecover !== null){ if(typeof m.crashRecover === "function"){ setTimeout(m.crashRecover,0);}}
e.preventDefault();
}
m.updateBounds = function(){
if(m.active){
m.bounds = m.element.getBoundingClientRect();
}
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === U) { m.callbacks = [callback]; }
else { m.callbacks.push(callback); }
} else { throw new TypeError("mouse.addCallback argument must be a function"); }
}
m.start = function (element, blockContextMenu) {
if (m.element !== U) { m.removeMouse(); }
m.element = element === U ? document : element;
m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
m.active = true;
m.updateBounds();
}
m.remove = function () {
if (m.element !== U) {
m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
m.element = m.callbacks = m.contextMenuBlocked = U;
m.active = false;
}
}
return mouse;
})();
resizeCanvas();
mouse.start(canvas,true);
onResize()
var lastTime = null;
window.addEventListener("resize",resizeCanvas);
function clearCTX(){
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h); // though not needed this is here to be fair across methods and demonstrat flicker
}
function dualUpdate(){
if(!dualTimersActive) {
dualTimersActive = true;
hdl1 = setInterval( clearCTX, 1000/60);
hdl2 = setInterval(() => display(10), 1000/60);
}
}
function timerUpdate(){
timer = performance.now();
if(!lastTime){
lastTime = timer;
}
var time = (timer-lastTime) / (1000/60);
lastTime = timer;
setTimeout(updateMethods[displayMethod],1000/60);
clearCTX();
display(10*time);
}
function updateRAF(){
clearCTX();
requestAnimationFrame(updateMethods[displayMethod]);
display(10);
}
function updateRAFTimer(timer){ // Main update loop
clearCTX();
requestAnimationFrame(updateMethods[displayMethod]);
if(!timer){
timer = 0;
}
if(!lastTime){
lastTime = timer;
}
var time = (timer-lastTime) / (1000/60);
display(10 * time);
lastTime = timer;
}
displayMethod = 1;
var updateMethods = [timerUpdate,updateRAF,dualUpdate,updateRAFTimer,timerUpdate]
updateMethods[displayMethod]();
/** SimpleFullCanvasMouse.js end **/
requestAnimationFrames
s callback is special, upon exit the back buffers are held until Vsync (no pixels move to display) This stops shear and flicker. –
Stockdale When we use setTimeout
or setInterval
it's redundant because setTimeout
or setInterval
perform a task almost 4 time in which only the last one is necessary so the first 3 is redundant and of no use just taking more CPU power but requestAnimationFrame takes just one step so we can roughly say requestAnimationFrame is almost 4 time better than setTimeout or setInterval
requestAnimationFrame
(RAF) multiple times in one function call means all of them will be resolved before the next draw, in which case it's not better. However, a typical RAF-based animation loop involves setting up the next RAF call. That approach guarantees it will only be called once per frame at most, ensuring that you avoid redundant work. –
Emera © 2022 - 2024 — McMap. All rights reserved.
requestIdleCallback
: developer.chrome.com/blog/using-requestidlecallback – Encomiastic