Why Is Requestanimationframe Better Than Setinterval or Settimeout

Why is requestAnimationFrame better than setInterval or setTimeout

High quality animation.

The question is most simply answered with. 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.

Shear

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.

Flicker

is caused when the canvas buffer is presented to the display buffer before the canvas has been fully rendered.

Frame skip

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)


A demo is worth a thousand words.

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 suks, but the gaming community swear 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 Blindman67const 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 **/

Is there any advantage to using requestAnimationFrame instead of setTimeout for debounce/throttle

RequestAnimationFrame is good if you want to make sure you don't do unnecessary work because you changed something two times between 2 frame updates.

In itself, the requestAnimationFrame is not useful for anything other than animation, because it can only wait 16.666 milliseconds (if there's no lag), thus you need to chain multiple together. But if you pair it with setTimeout, then you will be able to wait a sepcific amount of milliseconds and also make sure you draw everything in the correct time.

Should I use requestanimationframe or setTimeout?

Performance is always about specifics. Without more information on your app, (e.g. the specific code that renders your app ). It is hard to say how you should structure your code.

Generally, you should always use requestAnimationFrame. Especially for rendering.

Optionally store the delta time and multiply your animation attributes by that delta. This will create a smooth animation when the frame rate is not consistent.

I've also found random frame rate changes are usually related to garbage collection. Perhaps do some memory profiling to find if there are places you can avoid recreating objects each frame.

requestAnimationFrame is superior to setTimeout in nearly every way. It won't run as a background tab. It saves battery. It gives the browser more information about the type of app you are developing, and this lets the given browser make many safe performance increasing assumptions.

I highly recommend watching this talk by Nat Duca on browser performance.

Why is it recommend to nest setTimeout in requestAnimationFrame when scheduling something after a repaint?

Why is this the recommended solution? What exactly makes this the optimal way to schedule something right after a repaint?

Running code immediately after a repaint maximizes the chance that the DOM has been fully calculated, and thus minimizes the chance that querying the dom will cause a costly reflow. If you're not querying the dom anyway, then this isn't something you need to worry about.

requestAnimationFrame will schedule code to run immediately before the repaint, which is close to where you want to be but not quite. So then doing a setTimeout of 0 (or setImmediate on browsers that support it) will execute code as soon as it can after that. This won't guarantee that your code is the first thing to run after the repaint, but it's the best you can do given the apis at your disposal.

Using requestAnimationFrame() as setInterval() Javascript

You use requestAnimationFrame() by giving it a function reference. If you want to continuously call it with the same function, have the function recursively schedule another requestAnimationFrame(), it doesn't continuously fire as setInterval() does, it acts more like setTimeout().

If you want to delay the next request, then you aren't really getting a benefit over setInterval or setTimeout because you still have to schedule the next animation frame request after an amount of time. requestAnimationFrame() just says, "Hey browser, next time you go to paint the window, run this function while you're at it."

You're going to run into issues comparing colors like this. You might get an rgb(n, n, n) in some cases or maybe a hex string in others. I don't know the API specs.

Warning, the examples below will flicker colors very fast

function change() {  if ($("#foo").css("background-color") === "rgb(0, 128, 0)") {    $("#foo").css("background-color", "rgb(128, 0, 0)");  } else {    $("#foo").css("background-color", "green");  }    setTimeout(()=>requestAnimationFrame(change), 500);}
$("#foo").css("background-color", "red");requestAnimationFrame(change);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script><span id="foo">M</span>

How should I use requestAnimationFrame and setTimeout in parallel to make a better game loop?

So, I never found a great way to separate logic and drawing because JavaScript uses a single thread. No matter what I do the execution of the draw function may get in the way of the logic or vis versa. What I did do was find a way to execute them in the most timely manner possible while also ensuring constant time updates to the logic and optimized drawing using requestAnimation Frame. This system is set up to interpolate animations to make up for skipped frames should the device be too slow to draw at the desired frame rate. Anyway, here's my code.

var engine = {
/* FUNCTIONS. */
/* Starts the engine. */
/* interval_ is the number of milliseconds to wait between updating the logic. */
start : function(interval_) {
/* The accumulated_time is how much time has passed between the last logic update and the most recent call to render. */
var accumulated_time = interval_;
/* The current time is the current time of the most recent call to render. */
var current_time = undefined;
/* The amount of time between the second most recent call to render and the most recent call to render. */
var elapsed_time = undefined;
/* You need a reference to this in order to keep track of timeout and requestAnimationFrame ids inside the loop. */
var handle = this;
/* The last time render was called, as in the time that the second most recent call to render was made. */
var last_time = Date.now();

/* Here are the functions to be looped. */
/* They loop by setting up callbacks to themselves inside their own execution, thus creating a string of endless callbacks unless intentionally stopped. */
/* Each function is defined and called immediately using those fancy parenthesis. This keeps the functions totally private. Any scope above them won't know they exist! */
/* You want to call the logic function first so the drawing function will have something to work with. */
(function logic() {
/* Set up the next callback to logic to perpetuate the loop! */
handle.timeout = window.setTimeout(logic, interval_);

/* This is all pretty much just used to add onto the accumulated time since the last update. */
current_time = Date.now();
/* Really, I don't even need an elapsed time variable. I could just add the computation right onto accumulated time and save some allocation. */
elapsed_time = current_time - last_time;
last_time = current_time;

accumulated_time += elapsed_time;

/* Now you want to update once for every time interval_ can fit into accumulated_time. */
while (accumulated_time >= interval_) {
/* Update the logic!!!!!!!!!!!!!!!! */
red_square.update();

accumulated_time -= interval_;
}
})();

/* The reason for keeping the logic and drawing loops separate even though they're executing in the same thread asynchronously is because of the nature of timer based updates in an asynchronously updating environment. */
/* You don't want to waste any time when it comes to updating; any "naps" taken by the processor should be at the very end of a cycle after everything has already been processed. */
/* So, say your logic is wrapped in your RAF loop: it's only going to run whenever RAF says it's ready to draw. */
/* If you want your logic to run as consistently as possible on a set interval, it's best to keep it separate, because even if it has to wait for the RAF or input events to be processed, it still might naturally happen before or after those events, and we don't want to force it to occur at an earlier or later time if we don't have to. */
/* Ultimately, keeping these separate will allow them to execute in a more efficient manner rather than waiting when they don't have to. */
/* And since logic is way faster to update than drawing, drawing won't have to wait that long for updates to finish, should they happen before RAF. */

/* time_stamp_ is an argument accepted by the callback function of RAF. It records a high resolution time stamp of when the function was first executed. */
(function render(time_stamp_) {
/* Set up the next callback to RAF to perpetuate the loop! */
handle.animation_frame = window.requestAnimationFrame(render);

/* You don't want to render if your accumulated time is greater than interval_. */
/* This is dropping a frame when your refresh rate is faster than your logic can update. */
/* But it's dropped for a good reason. If interval > accumulated_time, then no new updates have occurred recently, so you'd just be redrawing the same old scene, anyway. */
if (accumulated_time < interval_) {
buffer.clearRect(0, 0, buffer.canvas.width, buffer.canvas.height);

/* accumulated_time/interval_ is the time step. */
/* It should always be less than 1. */
red_square.draw(accumulated_time / interval_);

html.output.innerHTML = "Number of warps: " + red_square.number_of_warps;

/* Always do this last. */
/* This updates the actual display canvas. */
display.clearRect(0, 0, display.canvas.width, display.canvas.height);
display.drawImage(buffer.canvas, 0, 0, buffer.canvas.width, buffer.canvas.height, 0, 0, display.canvas.width, display.canvas.height);
}
})();
},
/* Stops the engine by killing the timeout and the RAF. */
stop : function() {
window.cancelAnimationFrame(this.animation_frame);
window.clearTimeout(this.timeout);
this.animation_frame = this.timeout = undefined;
},
/* VARIABLES. */
animation_frame : undefined,
timeout : undefined
};

This is ripped straight out of one of my projects so there are a few variables in there that are defined elsewhere in code. red_square is one of those variables. If you want to check out the full example, take a look at my github page! userpoth.github.io Also, a side note, I tried using web workers to separate out logic and it was a miserable failure. Web workers are great when you have a lot of math to do and very few objects to pass between threads, but they can't do drawing and they are slow with big data transfers, at least in the context of game logic.



Related Topics



Leave a reply



Submit