Chapter 35: Canvas Element & 2D Graphics Basics

Canvas Animation & requestAnimationFrame

High-frame-rate rendering loops, 60fps/120fps VSync synchronization, Delta Time ($\Delta t$) physics equations, particle dynamics, and motion blur trails.

LEARNING OBJECTIVES
  • Understand why requestAnimationFrame (rAF) supersedes legacy setInterval/setTimeout animation loops.
  • Implement frame-rate-independent physics using microsecond Delta Time ($\Delta t$) calculations.
  • Construct a high-performance, garbage-collector-friendly 2D particle simulation engine.
  • Create visual effects such as neon glow compositing and translucent motion blur trails.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

The Stuttering Film Projector vs. The VSync Conductor

In the early days of web animation, developers drove animations using setInterval(render, 16.66). This approach had critical architectural flaws:

  • setInterval is blind to the monitor's physical refresh cycle, causing screen tearing and micro-stutters.
  • It continues running when the user minimizes the browser or changes tabs, wasting CPU cycles and draining mobile batteries.
  • On a $120\text{Hz}$ or $144\text{Hz}$ gaming display, setInterval locks the game to an artificial $60\text{Hz}$ ceiling.
+-----------------------------------------------------------------------------+
|                     setInterval vs. requestAnimationFrame                   |
+-----------------------------------------------------------------------------+

 1. setInterval(loop, 16) ---> The Blind Clock:
    - Fires every 16ms regardless of GPU status or monitor beam.
    - If the CPU lags, frames bunch up and drop unpredictably.
    - Keeps running in hidden background tabs.

 2. requestAnimationFrame(loop) ---> The Hardware VSync Conductor:
    - The browser waits for the monitor's physical refresh signal (VSync).
    - Executes right before the GPU paints the next physical frame.
    - Scales automatically: 60 FPS on 60Hz screens, 120 FPS on Apple ProMotion!
    - Automatically throttles to 0 FPS in hidden tabs to conserve battery.

Technical Deep Dive & Specifications

The Canonical Animation Loop Architecture

let lastTimestamp = 0;

function animationLoop(currentTimestamp) {
  // 1. Calculate Delta Time (Elapsed time in seconds)
  const rawDelta = (currentTimestamp - lastTimestamp) / 1000;
  const dt = Math.min(rawDelta, 0.1); // Clamp to 100ms to prevent "spiral of death"
  lastTimestamp = currentTimestamp;

  // 2. Clear Screen or Draw Fade Trail
  ctx.fillStyle = 'rgba(2, 6, 23, 0.2)'; // 20% alpha creates smooth motion blur
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // 3. Update Physics (Frame-rate independent)
  updateSimulation(dt);

  // 4. Render Visual Entities
  renderSimulation(ctx);

  // 5. Schedule Next Frame
  requestAnimationFrame(animationLoop);
}

// Kick off the loop
requestAnimationFrame(animationLoop);

Why Delta Time ($\Delta t$) is Mandatory

If you update position with a static increment:

x += 5; // Amateur Bug!
  • On a $60\text{Hz}$ monitor ($60\text{ FPS}$), the object moves $5 \times 60 = 300\text{ px/sec}$.
  • On a $144\text{Hz}$ gaming monitor ($144\text{ FPS}$), the object moves $5 \times 144 = 720\text{ px/sec}$ (more than twice as fast!).

To ensure identical physics across all devices, multiply velocity by Delta Time ($\Delta t$):

$$\text{Position}{t+1} = \text{Position}{t} + (\text{Velocity} \times \Delta t)$$

const speedPixelsPerSecond = 200;
x += speedPixelsPerSecond * dt; // Exact same physical speed on 30Hz, 60Hz, or 144Hz!

Particle Physics Dynamics

               [ Gravity Vector: g (Downward Acceleration) ]
                                     |
                                     v
   Position (x, y) <--- Velocity (vx, vy) += Acceleration * dt
          |
          v
   Boundary Check: If (y >= floor) -> vy = -vy * elasticity (Bounce & Dampen)

Particle Data Architecture

class Particle {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.vx = (Math.random() - 0.5) * 200; // Pixels per second
    this.vy = (Math.random() - 1.0) * 300;
    this.radius = Math.random() * 3 + 2;
    this.life = 1.0;                      // 1.0 = New, 0.0 = Dead
    this.decay = Math.random() * 0.5 + 0.3; // Decay per second
    this.color = `hsl(${Math.random() * 60 + 180}, 100%, 60%)`;
  }

  update(dt) {
    this.vy += 450 * dt; // Apply gravity (450 px/s^2)
    this.x += this.vx * dt;
    this.y += this.vy * dt;
    this.life -= this.decay * dt;
  }

  draw(ctx) {
    ctx.save();
    ctx.globalAlpha = Math.max(0, this.life);
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }
}

Glowing Particle Trails (globalCompositeOperation)

By setting ctx.globalCompositeOperation = 'lighter' (Additive Blending), overlapping glowing particles sum their RGB pixel values, producing intense radiant cores:

ctx.save();
ctx.globalCompositeOperation = 'lighter';
// Draw glowing particles...
ctx.restore();

💻 Interactive Code Playground

Starter Code

Line-by-Line Code Breakdown

  • Lines 50–74 (class Particle): Encapsulates position, velocity vectors, lifespan decay, and gravitational physics equations.
  • Line 88 (const dt = Math.min((currentTime - lastTime) / 1000, 0.1)): Computes Delta Time in seconds, clamped to $100\text{ms}$ to prevent physics explosion if the user leaves the tab and returns later.
  • Lines 107–108 (ctx.fillStyle = 'rgba(2, 6, 23, 0.25)'): Paints an 8-bit translucent dark overlay on each frame instead of calling clearRect(). Previous particle positions gently fade over several frames, generating smooth motion trails.
  • Line 112 (ctx.globalCompositeOperation = 'lighter'): Activates additive color blending; wherever multiple particles intersect, their RGB pigments add together to form luminous glowing whites and bright neons.
  • Lines 115–124: Reverse loop (for (let i = particles.length - 1; i >= 0; i--)) safely updates, renders, and removes dead particles (p.life <= 0) via splice() without index offset corruption.

Expected Browser Render Output


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...
+-------------------------------------------------------------+
| FPS: 60    Particles: 350    Click / Drag to Spawn          |
|                                                             |
|                          *  .                               |
|                       .  *  (Neon) *                        |
|                    *    /      \     .                      |
|                  .     *        *      *                    |
|                *   (Motion Trails)       .                  |
|          =============================================      |
|          ---------------- (Floor Bounce) -------------      |
+-------------------------------------------------------------+

🏋️ Hands-On Exercise

🎯 The Challenge: Build an Elastic Bouncing Balls Physics Sandbox with Mouse Repulsion

Instructions:

  1. Create a simulation with 50 bouncing balls of varying radiuses ($8\text{–}18\text{px}$) and random neon colors.
  2. Physics requirements:
    • Gravity: Accelerates balls downward ($g = 350\text{ px/s}^2$).
    • Elasticity / Restitution: When hitting the floor ($Y=360$), reverse $V_y$ and multiply by $-0.82$.
    • Wall collisions: When hitting left ($X=0$) or right ($X=550$) walls, reverse $V_x$ with $-0.9$ damping.
  3. Interactive Mouse Gravity Repulsion Field:
    • When the user hovers over the canvas, calculate the distance between the mouse $(mx, my)$ and every ball.
    • If distance is $< 100\text{px}$, apply a strong outward repulsive force pushing the ball away from the cursor!

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Using setInterval for Animation: Causes stuttering, desynchronizes from GPU hardware VSync, and wastes battery by running in background tabs. Always use requestAnimationFrame.
  2. Unclamped Delta Time (The "Spiral of Death"): If a user switches tabs for 10 seconds and returns, rawDelta will be $10\text{s}$. Without clamping (Math.min(dt, 0.1)), objects will jump thousands of pixels in a single frame and clip through walls.
  3. Garbage Collector Frame Choke: Instantiating thousands of temporary objects (new Particle(), new Vector()) inside the animation loop forces the JavaScript engine to pause execution for garbage collection, causing periodic frame rate drops.

💡 Pro Tips

  1. Object Pooling Pattern: Instead of calling new Particle() and array.splice(), pre-allocate a fixed array of 1,000 particle objects. Mark dead particles as active = false and revive them when needed to achieve zero-allocation garbage-free $60\text{ FPS}$.
  2. Sub-Stepping Fast Physics: If balls travel faster than their own radius per frame ($V > r / \Delta t$), they can tunnel through boundaries. Divide the update step into 2 or 4 sub-steps (dt / 4) to ensure collision precision.
  3. Offscreen Canvas & Web Workers: For massive simulations (100,000+ particles), offload physics calculations and Canvas rendering to a Web Worker using OffscreenCanvas and transferControlToOffscreen().

📌 Key Takeaways

  • requestAnimationFrame synchronizes directly with the display's hardware VSync refresh rate (60Hz, 120Hz, 144Hz).
  • Physics must always be multiplied by Delta Time ($\Delta t$) to maintain consistent velocity across varying monitor refresh rates.
  • Always clamp Delta Time (Math.min(dt, 0.1)) to protect against physics explosions after returning from inactive tabs.
  • Semi-transparent fillRect() clears generate motion blur trails, and globalCompositeOperation = 'lighter' produces luminous glowing particles.
  • Avoid memory allocations inside the render loop to prevent garbage collection frame drops.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Why is requestAnimationFrame superior to setInterval(draw, 16.6) for web animation?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

What is the purpose of multiplying velocity vectors by Delta Time ($\Delta t$) in physics calculations?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

How can a developer create a continuous fading motion blur trail behind moving canvas particles?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP