Let's Build a Game Engine in HTML
Chapter 2
2 / 9

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.

requestAnimationFrame
frame clear
elapsed time
animation phase

How to do it

  1. 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. 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. 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.

Type-along practice
Photo layout
The Game Loop
1 / 64 lines

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.

Prewritten engine scriptmistakes 0
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 };
15
16const canvas = document.getElementById('c');
17const ctx = canvas.getContext('2d');
18const W = canvas.width, H = canvas.height;
19const hud = document.getElementById('hud');
20
21const SPEED = 1;
22const RADIUS = 90;
23const TRAIL = 0.15;
24const DOT_SIZE = 10;
25const PULSE = 0.35;
26
27let t0 = performance.now();
28let frames = 0;
29let fps = 0;
30let fpsT = t0;
31
32function 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 }
39
40 // Soft clear for trail
41 ctx.fillStyle = `rgba(11,16,24,${TRAIL})`;
42 ctx.fillRect(0, 0, W, H);
43
44 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;
50
51 // Path guide
52 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();
56
57 // Glow + core
58 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();
63
64 ctx.fillStyle = '#5eead4';
65 ctx.beginPath(); ctx.arc(px, py, r, 0, Math.PI * 2); ctx.fill();
66
67 hud.textContent = 'Chapter 2 · loop ' + fps + ' fps · t=' + t.toFixed(1) + 's';
68 requestAnimationFrame(loop);
69}
70
71// Seed opaque background once
72ctx.fillStyle = '#0b1018';
73ctx.fillRect(0, 0, W, H);
74requestAnimationFrame(loop);
75
76})();
Your lineline 1
(function () {

Auto-animating demo — watch the loop spin and tweak sliders live. · Sliders update the play window after a short pause.

Chapter solution
Reference
Live-bound HTML
Play window
Live