Game Loop

A Game Loop with requestAnimationFrame

requestAnimationFrame(callback) runs the callback before the next repaint with a timestamp; calling it again from inside makes a loop at the display's rate that pauses in background tabs, replacing old setTimeout() loops. Move things by elapsed time, not frame count.

A book tumbling at 240 pixels per second, with faded earlier frames as a trailHTML
<canvas id="c" style="width:600px; height:80px"></canvas><script src="hidpi.js"></script>
<pre id="out" style="margin:0"></pre>
<script>
  const ctx = hidpi(c);  let x = 0, start, last, frames = 0;
  function frame(now) {
    start ??= now;  const dt = (now - (last ?? now)) / 1000;  last = now;  // seconds
    ctx.fillStyle = 'rgb(255 255 255 / 0.3)';  ctx.fillRect(0, 0, 600, 80);  // fade
    x = Math.min(x + 240 * dt, 540);  frames++;
    ctx.save();  ctx.translate(30 + x, 40);  ctx.rotate(x / 60);
    ctx.fillStyle = '#b5452f';  ctx.fillRect(-12, -18, 24, 36);  ctx.restore();
    if (x < 540) return requestAnimationFrame(frame);
    out.textContent = `${frames} frames in ${((now - start) / 1000).toFixed(2)} s`;
    __done = true;
  }
  requestAnimationFrame(frame);
</script>
Browser output of Listing 2.21
Browser output of 21

A translucent white fill instead of a clear leaves the trail. Clamp dt after a pause, or a returning tab makes objects jump.

A requestAnimationFrame loop that moves books by elapsed time, with a fading trailHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; font: 13px system-ui, sans-serif; background: #fafaf7; }
  canvas { width: 100%; max-width: 600px; height: 160px; display: block; background: #fff; border: 1px solid #ddd; }
</style>
<canvas id="c"></canvas>
<p id="out"></p>
<script>
  function hidpi(c, r = devicePixelRatio) {
    c.width = c.clientWidth * r;  c.height = c.clientHeight * r;
    const ctx = c.getContext('2d');  ctx.scale(r, r);  return ctx;
  }
  const ctx = hidpi(document.getElementById('c'));
  const out = document.getElementById('out');
  // Three books with speeds in pixels per second, independent of the display's frame rate
  const books = [
    { x: 30, y: 40, speed: 240, color: '#b5452f' },
    { x: 30, y: 80, speed: 160, color: '#1f5f8b' },
    { x: 30, y: 120, speed: 90, color: '#3f7d3a' }
  ];
  let last, frames = 0, elapsed = 0;

  function frame(now) {
    // Seconds since the previous frame, clamped so a returning tab doesn't make books jump
    const dt = Math.min((now - (last ?? now)) / 1000, 0.1);
    last = now;  frames++;  elapsed += dt;

    // A translucent white fill instead of clearRect leaves a trail
    ctx.fillStyle = 'rgb(255 255 255 / 0.3)';  ctx.fillRect(0, 0, 600, 160);
    for (const b of books) {
      b.x += b.speed * dt;
      if (b.x > 580) b.x = 20;                    // wrap around
      ctx.save();  ctx.translate(b.x, b.y);  ctx.rotate(b.x / 60);
      ctx.fillStyle = b.color;  ctx.fillRect(-10, -15, 20, 30);
      ctx.restore();
    }
    if (frames % 30 === 0) out.textContent = `${frames} frames in ${elapsed.toFixed(2)} s ≈ ${Math.round(frames / elapsed)} fps`;
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
</script>