15

As I understand it, usage of the JS requestAnimationFrame API is intended for cases where the framerate isn't in need of being controlled, but I have a use case where it's essential that a <canvas> only updates at a certain fps interval that may be anywhere between 1 and 25 (between 1 and 25 frames per second, that is). Can I then somehow still effectively use rAF to get at the optimizations it offers?

This question has similarities to mine, but the accepted answer there made close to zero sense to me in the context of that question.

I have two possible solutions for this. The first one involves using a while loop to halt the execution of the script for a specified delay before calling requestAnimationFrame from within the callback. In the example where I saw this, it effectively limited the fps of the animation, but it also seemed to slow down the entire tab. Is this still actually a good solution? The second alternative, as mentioned in the question I linked to above, calls requestAnimationFrame within a setInterval. To me that seems a bit convoluted, but it could be that's the best option?

Or is there a better alternative to accomplish this?

Community
  • 1
  • 1
fredrikekelund
  • 2,007
  • 2
  • 21
  • 33

3 Answers3

14

Yoshi's answer is probably the best code solution to this problem. But still I'll mark this answer as correct, because after some research I basically found that my question was invalid. requestAnimationFrame is really meant to keep frame rates as high as possible, and it optimizes for scenarios where animation is meant to be kept consistent and smooth.

Worth noting though is that you don't need requestAnimationFrame to get optimization (even though rAF was touted as a great performance booster) since browsers still optimize regular drawing of a <canvas>. For example, when a tab isn't focused, Chrome for one stops drawing its canvases.

So my conclusion was that this question was invalid. Hope this helps anyone who was wondering something similar to me.

fredrikekelund
  • 2,007
  • 2
  • 21
  • 33
  • 1
    I think you're absolutely correct with your conclusion. So making this the accepted answer is probably a lot more helpful for future readers. – Yoshi Mar 08 '12 at 16:18
9

This is just a proof of concept.

All we do is set our frames per second and intervals between each frame. In the drawing function we deduct our last frame’s execution time from the current time to check whether the time elapsed since the last frame is more than our interval (which is based on the fps) or not. If the condition evaluates to true, we set the time for our current frame which is going to be the “last frame execution time” in the next drawing call.

var Timer = function(callback, fps){
  var now = 0;
  var delta = 0;
  var then = Date.now();

  var frames = 0;
  var oldtime = 0;

  fps = 1000 / (this.fps || fps || 60);

  return requestAnimationFrame(function loop(time){
    requestAnimationFrame(loop);

    now = Date.now();
    delta = now - then;

    if (delta > fps) {
      // Update time stuffs
      then = now - (delta % fps);

      // Calculate the frames per second.
      frames = 1000 / (time - oldtime)
      oldtime = time;

      // Call the callback-function and pass
      // our current frame into it.
      callback(frames);
    }
  });
};

Usage:

var set;
document.onclick = function(){
  set = true;
};

Timer(function(fps){
  if(set) this.fps = 30;
  console.log(fps);
}, 5);

http://jsfiddle.net/ARTsinn/rPAeN/

yckart
  • 32,460
  • 9
  • 122
  • 129
  • 3
    Would you consider adding some narrative to explain why this code works, and what makes it an answer to the question? This would be very helpful to the person asking the question, and anyone else who comes along. – Andrew Barber Apr 06 '13 at 22:22
  • There is quite some stuff going on here - but that's a pretty clever solution. I might try and integrate this into my code, will try to get back here and add some edits with further explanations as soon as I got it working if nothing else has happened before then. – fredrikekelund Apr 17 '13 at 21:08
  • 2
    I've added a bit explanation text. Hope that helps. – yckart Apr 18 '13 at 18:58
5

What you can do, though I don't really know if this is really any better is:

  • render to an invisible context with requestAnimationFrame
  • update a visible context with setInterval using a fixed fps

Example:

<canvas id="canvas"></canvas>​

<script type="text/javascript">
  (function () {
    var
      ctxVisible = document.getElementById('canvas').getContext('2d'),
      ctxHidden = document.createElement('canvas').getContext('2d');

    // quick anim sample
    (function () {
      var x = 0, y = 75;

      (function animLoop() {
        // too lazy to use a polyfill here
        webkitRequestAnimationFrame(animLoop);

        ctxHidden.clearRect(0, 0, 300, 150);
        ctxHidden.fillStyle = 'black';
        ctxHidden.fillRect(x - 1, y - 1, 3, 3);

        x += 1;
        if (x > 300) {
          x = 0;
        }
      }());
    }());

    // copy the hidden ctx to the visible ctx on a fixed interval (25 fps)
    setInterval(function () {
      ctxVisible.putImageData(ctxHidden.getImageData(0, 0, ctxHidden.canvas.width, ctxHidden.canvas.height), 0, 0);
    }, 1000/40);
  }());
</script>

Demo: http://jsfiddle.net/54vWN/

Yoshi
  • 54,081
  • 14
  • 89
  • 103
  • 1
    Thanks a lot for your answer and the fiddle -- much appreciated! However I think my question really is invalid, so I don't feel as though I can mark this as the accepted answer. Upvoted though, and thanks again :) – fredrikekelund Mar 08 '12 at 15:53