The Game Loop
Time becomes continuous with requestAnimationFrame
Chapter goal
Drive the canvas every frame so the world can animate. Learn clear → update → draw and how a simple elapsed-time clock powers motion.
How to do it
- 1
Schedule the next frame
requestAnimationFrame(loop) asks the browser to call your function before the next paint (~60fps). At the end of the function, schedule again — that is the loop.
- 2
Clear, then draw
Each frame wipe the previous pixels (clearRect or a full fill), then redraw the current state. Leaving old pixels creates trails unless you want them.
- 3
Use time, not frame counts
performance.now() (or the rAF timestamp) gives milliseconds. Animating with time keeps motion smooth if the frame rate dips.
Live lab
Solution code updates with sliders. Tinker is your sandbox. Play runs whichever document you launch.
Live settings
Every slider injects into the engine constants for this chapter.
The chapter engine script is prewritten below. Type it line by line — each line explains why it belongs there. Matching a line advances automatically; Enter re-checks if you are stuck.
1(function () {2 "use strict";3 // Single boot — if this document is re-parsed, abandon prior work.4 if (window.__engineBooted) return;5 window.__engineBooted = true;6 let alive = true;7 window.addEventListener("pagehide", function () { alive = false; }, { once: true });8 const raf = window.requestAnimationFrame.bind(window);9 window.requestAnimationFrame = function (cb) {10 return raf(function (ts) {11 if (!alive) return;12 try { cb(ts); } catch (err) { console.error(err); alive = false; }13 });14 };1516const canvas = document.getElementById('c');17const ctx = canvas.getContext('2d');18const W = canvas.width, H = canvas.height;19const hud = document.getElementById('hud');2021const SPEED = 1;22const RADIUS = 90;23const TRAIL = 0.15;24const DOT_SIZE = 10;25const PULSE = 0.35;2627let t0 = performance.now();28let frames = 0;29let fps = 0;30let fpsT = t0;3132function loop(now) {33 const t = (now - t0) / 1000;34 frames++;35 if (now - fpsT > 500) {36 fps = Math.round(frames * 1000 / (now - fpsT));37 frames = 0; fpsT = now;38 }3940 // Soft clear for trail41 ctx.fillStyle = `rgba(11,16,24,${TRAIL})`;42 ctx.fillRect(0, 0, W, H);4344 const cx = W/2, cy = H/2;45 const angle = t * SPEED;46 const px = cx + Math.cos(angle) * RADIUS;47 const py = cy + Math.sin(angle) * RADIUS * 0.55;48 const pulse = 1 + Math.sin(t * 4) * PULSE * 0.35;49 const r = DOT_SIZE * pulse;5051 // Path guide52 ctx.strokeStyle = 'rgba(94,234,212,0.12)';53 ctx.beginPath();54 ctx.ellipse(cx, cy, RADIUS, RADIUS * 0.55, 0, 0, Math.PI * 2);55 ctx.stroke();5657 // Glow + core58 const g = ctx.createRadialGradient(px, py, 0, px, py, r * 3);59 g.addColorStop(0, 'rgba(94,234,212,0.45)');60 g.addColorStop(1, 'transparent');61 ctx.fillStyle = g;62 ctx.beginPath(); ctx.arc(px, py, r * 3, 0, Math.PI * 2); ctx.fill();6364 ctx.fillStyle = '#5eead4';65 ctx.beginPath(); ctx.arc(px, py, r, 0, Math.PI * 2); ctx.fill();6667 hud.textContent = 'Chapter 2 · loop ' + fps + ' fps · t=' + t.toFixed(1) + 's';68 requestAnimationFrame(loop);69}7071// Seed opaque background once72ctx.fillStyle = '#0b1018';73ctx.fillRect(0, 0, W, H);74requestAnimationFrame(loop);7576})();
Auto-animating demo — watch the loop spin and tweak sliders live. · Sliders update the play window after a short pause.