Chapter 8: 2D Visuals with Canvas
The HTML5 <canvas> element provides a procedural, pixel-based API for rendering 2D graphics. Unlike SVG (Scalable Vector Graphics), which is part of the DOM and retained in memory, the Canvas is "immediate mode"—once a shape is drawn, the browser "forgets" it, retaining only the resulting pixels. This makes it ideal for high-performance animations, data visualizations, and game engines.
I. The Canvas 2D Context
To draw on a canvas, you must obtain its rendering context. Modern browsers use GPU-accelerated rasterization to convert drawing commands into pixels.
1. Coordinate System
The Canvas coordinate system starts at (0,0) in the top-left corner. X increases to the right, and Y increases downwards.
II. Comprehensive API Reference
1. Core Drawing & Paths
| Method | Syntax | Description |
|---|---|---|
fillRect() | ctx.fillRect(x, y, w, h) | Draws a filled rectangle. |
strokeRect() | ctx.strokeRect(x, y, w, h) | Draws a rectangular outline. |
clearRect() | ctx.clearRect(x, y, w, h) | Sets pixels in the area to transparent black. |
beginPath() | ctx.beginPath() | Starts a new path. |
moveTo() | ctx.moveTo(x, y) | Moves pen to (x,y). |
lineTo() | ctx.lineTo(x, y) | Draws line to (x,y). |
arc() | ctx.arc(x,y,r,sA,eA,ccw) | Circular arc (radians). |
bezierCurveTo() | ctx.bezierCurveTo(...) | Cubic Bézier curve (6 params). |
clip() | ctx.clip() | Turns the current path into a masking region. |
isPointInPath() | ctx.isPointInPath(x, y) | Hit Detection: Returns true if (x,y) is inside path. |
2. Text API & Layout
| Method | Syntax | Description |
|---|---|---|
fillText() | ctx.fillText(str, x, y, maxW?) | Draws filled text. |
strokeText() | ctx.strokeText(str, x, y, maxW?) | Draws text outline. |
measureText() | ctx.measureText(str) | Returns TextMetrics (width, bounding box, etc.). |
3. Image API (drawImage Polymorphism)
The drawImage method supports three distinct parameter signatures.
| Form | Parameters | Use Case |
|---|---|---|
| Simple | img, dx, dy | Draw at original size. |
| Scaled | img, dx, dy, dw, dh | Draw and scale to fit. |
| Slicing | img, sx, sy, sw, sh, dx, dy, dw, dh | Sprite Animation: Extract sub-region. |
4. Compositing & Blending
The globalCompositeOperation property defines how new shapes are drawn relative to existing pixels.
III. Implementation Patterns
1. Basic Animation & Interaction
Use requestAnimationFrame for synchronization and save/restore for state management.
// Implementation: Rotating a Square
function drawRotated(ctx, x, y, size, angle) {
ctx.save();
ctx.translate(x + size/2, y + size/2);
ctx.rotate(angle);
ctx.fillRect(-size/2, -size/2, size, size);
ctx.restore();
}
// Interactivity: Painting Tool
canvas.addEventListener('mousemove', (e) => {
if (e.buttons !== 1) return;
const rect = canvas.getBoundingClientRect();
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
ctx.stroke();
});
2. Sprite Animation (Slicing)
function drawSpriteFrame(img, frame, width, height, dx, dy) {
ctx.drawImage(img, frame * width, 0, width, height, dx, dy, width, height);
}
3. Clipping & Lighting (Gradients)
// Circular Masking (Avatars)
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.clip();
ctx.drawImage(img, 50, 50, 100, 100);
4. Procedural & Pixel Manipulation
- Recursive L-Systems: Used for generating trees/fractals.
- Convolution Kernels: Manipulating
ImageDatafor Sharpen/Blur filters.
// Hit Detection: Pixel-Alpha Check
const pixel = ctx.getImageData(x, y, 1, 1).data;
if (pixel[3] > 0) console.log('Hit!');
5. Multi-threaded Offscreen Rendering
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
IV. Core Engineering Standards
1. Performance Mandates
To achieve a consistent 60FPS (16.6ms frame budget), you must optimize how the browser and GPU interact with the canvas.
A. Canvas Layering (The "Static-Dynamic" Split)
Redrawing a complex background in every frame is an anti-pattern. Instead, use multiple overlapping canvas elements using CSS z-index.
- Background Layer: Draw static terrain/UI once.
- Dynamic Layer: Draw only moving entities (cleared every frame).
- Benefit: Reduces the number of pixels the GPU must rasterize per frame by up to 90%.
B. High-DPI (Retina) Scaling
On modern displays, window.devicePixelRatio is often 2 or 3. If you don't scale your canvas, your graphics will appear blurry because the browser upscales a low-resolution buffer.
function setupHighDPI(canvas) {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
// 1. Scale the internal drawing buffer
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
// 2. Use CSS to keep the display size the same
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
// 3. Normalize coordinates so you can draw using CSS pixels
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
return ctx;
}
C. Path Batching & State Changes
Every call to stroke(), fill(), or changing ctx.fillStyle involves a state switch in the GPU pipeline.
- Inefficient: Loop { set color, draw shape, stroke }.
- Efficient: Group shapes by color -> Loop { draw path } -> stroke once.
- Technical Result: Minimizes "Draw Calls" sent to the GPU, significantly reducing CPU-to-GPU overhead.
D. The Integer Grid
Drawing at non-integer coordinates (e.g., x: 10.5) forces the browser to perform sub-pixel anti-aliasing. This involves sampling adjacent pixels and blending colors, which is computationally expensive and makes graphics look "soft".
- Production Standard: Always use
Math.floor()or bitwise| 0on coordinates before drawing. - Result: Ensures pixel-perfect alignment and bypasses the anti-aliasing engine.
E. Avoiding DOM Access in Loops
Reading properties like canvas.width or window.innerWidth inside an animation loop can trigger Synchronous Layout (Reflow).
- Architectural Mandate: Cache all required dimensions in local variables outside the
requestAnimationFrameloop.
2. Memory Mandates
- Object Recycling: Use Object Pooling for particle systems to prevent Garbage Collection pauses.
- Buffer Management: Reuse
Uint8ClampedArraybuffers for high-frequencyImageDataprocessing.