Throttling (aka Forcing a Framerate on) requestAnimationFrame!

With devices (like the new iPad) featuring displays over 60Hz, there is a good chance that browsers will be running at rates greater than 60Hz as well. That’s great for everyone except those of us who used requestAnimationFrame thinking that rates will stay fixed at 60Hz,

To force a frame rate, check out the following snippet:

var currentTime;
var elapsedTime;
var startTime;

// set the frame rate you want!
var fps = 60;

var interval = 1000 / fps;

function loop() {
  currentTime = performance.now();
  elapsedTime = currentTime - startTime;

  if (elapsedTime > interval) {
    // this seems weird, but it is done to account for
    // the time it takes to display a frame
    startTime = currentTime - (elapsedTime % interval);

    //
    // Your animation code goes below!
    //
  }
  requestAnimationFrame(loop);
}

// Start the timer!
function start() {
  startTime = performance.now();
  requestAnimationFrame(loop);
}
start();

I’ll write this up more formally with some examples shortly, but this may help for now :slight_smile:

If you have no idea what I am talking about, read up on requestAnimationFrame in this tutorial.

Cheers,
Kirupa

Here is an example of this code in action:

<!DOCTYPE html>
<html>

<head>
  <title>requestAnimationFrame Throttling Example</title>
  <style>
    body {
      margin: 50px;
    }
    #myCanvas {
      border: 5px solid #EEE;
    }
    #container p {
      font-family: sans-serif;
      font-size: 12pt;
    }
    #container div p {
      display: inline-block;
    }
    #fpsDropdown {
      font-size: 16px;
      font-weight: bold;
    }
  </style>
</head>

<body>
  <div id="container">
      <canvas id="myCanvas" height="400" width="400"></canvas>
      <div>
        <p>Frames per second:</p>
        <select id="fpsDropdown">
          <option value="1">1</option>
          <option value="5">5</option>
          <option value="15">15</option>
          <option value="30">30</option>
          <option selected="selected" value="60">60</option>
          <option value="120">120</option>
          <option value="240">240</option>
        </select>
      <div>
  </div>

  <script>

    var currentTime;
    var elapsedTime;
    var startTime;

    // set the frame rate you want!
    var fps = 60;

    var interval = 1000 / fps;

    var mainCanvas = document.querySelector("#myCanvas");
    var mainContext = mainCanvas.getContext('2d');

    var canvasWidth = mainCanvas.width;
    var canvasHeight = mainCanvas.height;

    var selector = document.querySelector("#fpsDropdown");
    selector.addEventListener("change", updateFPS, false);

    function updateFPS(e) {
      var selectIndex = selector.options.selectedIndex;
      fps = parseInt(selector.options[selectIndex].value);
      interval = 1000 / fps;

      console.log("New fps is: " + fps);

      start();
    }

    // this array contains a reference to every circle that you will create
    var circles = new Array();

    //
    // The Circle "constructor" is responsible for creating the circle
    // objects and defining the various properties they have
    //
    function Circle(angle, sign, radius, rotationRadius, initialX, initialY) {
        this.angle = angle;
        this.sign = sign;
        this.radius = radius;
        this.rotationRadius = rotationRadius;
        this.initialX = initialX;
        this.initialY = initialY;
        this.incrementer = .01 + Math.random() * .1;
    }

    Circle.prototype.update = function () {

        this.angle += this.incrementer;

        this.currentX = this.initialX + this.rotationRadius * Math.cos(this.angle);
        this.currentY = this.initialY + this.rotationRadius * Math.sin(this.angle);

        if (this.angle >= (Math.PI * 2)) {
            this.angle = 0;
            this.incrementer = .01 + Math.random() * .1;
        }

        // The following code is responsible for actually drawing the circle
        mainContext.beginPath();
        mainContext.arc(this.currentX, this.currentY, this.radius,
                        0, Math.PI * 2, false);
        mainContext.closePath();
        mainContext.fillStyle = 'rgba(0, 51, 204, .1)';
        mainContext.fill();
    };

    //
    // This function creates the circles that you end up seeing
    //
    function createCircles() {
        // change the range of this loop to adjust the number of circles you see
        for (var i = 0; i < 50; i++) {
            var radius = 5 + Math.random() * 100;
            var initialX = canvasWidth / 2;
            var initialY = canvasHeight / 2;
            var rotationRadius = 1 + Math.random() * 30;
            var angle = Math.random() * 2 * Math.PI;

            var signHelper = Math.floor(Math.random() * 2);
            var sign;

            // Randomly specify the direction the circle will be rotating
            if (signHelper == 1) {
                sign = -1;
            } else {
                sign = 1;
            }

            // create the Circle object
            var circle = new Circle(angle,
                                    sign,
                                    radius,
                                    rotationRadius,
                                    initialX,
                                    initialY);
            circles.push(circle);
        }
        // call the draw function approximately 60 times a second
        start();
    }
    createCircles();


    function loop() {
      currentTime = performance.now();
      elapsedTime = currentTime - startTime;

      if (elapsedTime > interval) {
        // this seems weird, but it is done to account for
        // the time it takes to display a frame
        startTime = currentTime - (elapsedTime % interval);

        mainContext.clearRect(0, 0, canvasWidth, canvasHeight);
        mainContext.fillStyle = '#F6F6F6';
        mainContext.fillRect(0, 0, canvasWidth, canvasHeight);

        for (var i = 0; i < circles.length; i++) {
            var circle = circles[i];
            circle.update();
        }
      }
      requestAnimationFrame(loop);
    }

    // Start the timer!
    function start() {
      startTime = performance.now();
      requestAnimationFrame(loop);
    }
    start();
  </script>

</body>

</html>

I think it’d be useful to provide advice on when & why someone would pick a particular frame rate. When do I pick 120, 60, 30, 24, 12, etc.

Good point! I’ll be sure to mention that in the article :slight_smile: