AlexKorzin
AlexKorzin

Reputation: 43

Can I call requestAnimationFrame multiple times?

Is it right to call requestAnimationFrame 2 or more times? For example:

function animate(){
  // ** Animate here
  if( something )
  requestAnimationFrame(animate);
}

function GameLoop() {
    // ** Some code
    if ( somethingElse )
    animate();
}
setInterval(GameLoop, 1000);

//Main render function
function render(){
  // ** Draw and animate objects
  requestAnimationFrame(render)
}

render();

I gues it's not right to do like that. So how can I put all my animations in one RAF?

Upvotes: 3

Views: 4209

Answers (3)

Kaiido
Kaiido

Reputation: 136598

What does requestAnimationFrame(cb) do?

It pushes the callback cb in a list of animation frame callbacks and marks the Document as animated.

It will then visit this list and execute all the callbacks there in a single call. The when this happens is based on browser's heuristics, but corresponds to the painting frame, which is an Event Loop where a should render flag was raised and which marks when the update the rendering algorithm will kick in.
This means that all the callbacks will get executed before the browser has a chance to paint to the screen.

This also means that yes, you can "call requestAnimationFrame multiple times".

requestAnimationFrame(funcA);
requestAnimationFrame(funcB);
// ~ same as
requestAnimationFrame((t)=> {
  funcA(t);
  funcB(t);
});

It will indeed be wrapped in the same execution sample, like if wrapped in the same outer function, with thus no apparent difference (except that thrown error won't block next callbacks).

But this doesn't mean that you should...

It is okay to do it if you have multiple unrelated animations that needs to run concurrently, however, for the same animation, having two simultaneous calls to requestAnimationFrame is usually a problem.

It is very hard to know where in the stack of callbacks yours has been added, and thus in which order it will get executed. All you can be sure out of the box is that all the recursive calls (the ones from a func = t => requestAnimation(func); loop) will have executed before the ones coming from outside (like user events). For more on this, see this Q/A

So if you ever have more than a single possible entry from outside, you can't know which one will happen first => You don't know what you have on screen.

The solution is quite simple in your case:

You don't need to call requestAnimationFrame from anywhere else than in your render loop.

Split your logic more thoroughly: a global loop which will call an update function then a draw function.
The update function will be responsible of updating all the objects in the scene, and only to update these, maybe based on some external semaphors/variables.
The drawing function will be responsible of drawing all the objects that should get rendered, and that's it.

None of these functions should be responsible of anything else, your main loop is the engine.

External events will just update some variables in your scene's objects, but won't ever call the drawing function, nor will they be responsible of more than just starting or stopping the engine if needed.

const w = canvas.width = 500;
const h = canvas.height = 300;
const ctx = canvas.getContext('2d');
let invertX = false;

const scene = {
  objects: [],
  update(t) {
    // here we only update the objects
    this.objects.forEach(obj => {
      if(invertX) {
        obj.dx *= -1;
      }
      obj.x += obj.dx;
      obj.y += obj.dy;
      if(obj.x > w) obj.x = (obj.x - w) - obj.w;
      if(obj.x + obj.w < 0) obj.x = w - (obj.x + obj.w);
      if(obj.y > h) obj.y = (obj.y - h) - obj.h;
      if(obj.y + obj.h < 0) obj.y = h - (obj.y + obj.h);
    });
    invertX = false;
  },
  draw() {
    // here we only draw
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0,0,canvas.width, canvas.height);
    this.objects.forEach(obj => {
      ctx.fillStyle = obj.fill;
      ctx.fillRect(obj.x, obj.y, obj.w, obj.h);
    }); 
  }
}

function mainLoop() {
  scene.update();
  scene.draw();
  requestAnimationFrame(mainLoop);
}

for(let i=0; i<50; i++) {
  scene.objects.push({
    x: Math.random() * w,
    y: Math.random() * h,
    w: Math.random() * w / 5,
    h: Math.random() * h / 5,
    dx: (Math.random() * 3 - 1.5),
    dy: (Math.random() * 3 - 1.5),
    fill: '#' + Math.floor(Math.random()*0xFFFFFF).toString(16)
  });
}
// every second do something external
setInterval(() => {
  invertX = true;
}, 1000);
// make one follow the cursor
onmousemove = e => {
  const evtX = e.clientX - canvas.offsetLeft;
  const evtY = e.clientY - canvas.offsetTop;
  const obj = scene.objects[0];
  const dirX = Math.sign(evtX - obj.x);
  const dirY = Math.sign(evtY - obj.y);
  obj.dx = Math.abs(obj.dx) * dirX;
  obj.dy = Math.abs(obj.dy) * dirY;
}

mainLoop();
<canvas id="canvas"></canvas>

Upvotes: 2

Graham P Heath
Graham P Heath

Reputation: 7389

Can I call requestAnimationFrame multiple times?

Yes, you can. requestAnimationFrame executes functions in order of addition.

You should use one requestAnimationFrame loop if:

  • Performance is the most important thing in your app.
  • If your app is dropping frames (or you fear that it might at some point on your roadmap).

This is a 'saturated' requestAnimationFrame.

You can use more than one requestAnimationFrame loop if:

  • Your tasks are simple.
  • You trust your tasks to run completely before the next frame.

This is an 'unsaturated' requestAnimationFrame.

Take for example the task of drawing 3 circles to a canvas.

In a 'saturated' codebase, execution time matters. The circles are competing for the required processing power. The number of RAF loops could be the difference between completing the frame in time, or rendering an incomplete frame. In this case, operations per second is a useful measurement.

In our 'unsaturated' code base execution time does not matter. The circles are not competing for the required processing power. In this case, the code requires no further optimization.

Completion: JSPerf - RAF Single vs Multiple draw calls Async and Unsaturated

Operations per second: JSPerf - Single RAF draw calls vs Multiple RAF draw calls

Upvotes: 3

aptriangle
aptriangle

Reputation: 1428

First of all, both setInterval and requestAnimation frame will be regularly calling animate, setInterval every 1000 ms and requestAnimation every frame. It's probably better to stick with only one of the two.

Creating multiple loops using requestAnimation isn't necessarily wrong, but most people have one loop that calls animation functions then rendering functions. People will argue about performance, but it depends on how the browser optimizes. Personally, seeing two different loops feels like fingernails scraping on a chalkboard, because I've always seen people doing it the other way, but it should work.

If you do want to consolidate:

function animate(){
    // ** Animate here
    if( something )
}
//Main render function
function render(){
    // ** Draw and animate objects
}

function GameLoop() {
    // ** Some code
    if ( somethingElse )
    animate();
    render();
    requestAnimationFrame(GameLoop);
}


GameLoop();

Upvotes: 0

Related Questions