HTML Canvas
The <canvas> element provides a bitmap drawing surface that is rendered via JavaScript. Unlike SVG which retains a DOM representation of every shape, canvas operates as an immediate-mode API — once something is drawn, it becomes pixels and is forgotten by the system.
Canvas supports two primary rendering contexts: 2d for raster 2D graphics, and webgl/webgl2 for hardware-accelerated 3D. This guide focuses on the 2D context, which is ideal for data visualizations, image processing, game rendering, and real-time animation.
The canvas element is declared in HTML and accessed via JavaScript. The getContext("2d") method returns a drawing context with methods for rendering shapes, text, images, and more.
| 1 | <canvas id="myCanvas" width="800" height="600"> |
| 2 | <p>Your browser does not support the canvas element.</p> |
| 3 | </canvas> |
The width and height attributes set the drawing surface size in logical pixels. CSS sizing only affects display size, not resolution. Always set these attributes explicitly — omitting them defaults to 300x150.
| 1 | const canvas = document.getElementById("myCanvas"); |
| 2 | const ctx = canvas.getContext("2d"); |
| 3 | |
| 4 | // Coordinate system: origin (0,0) at top-left |
| 5 | // x increases rightward, y increases downward |
| 6 | ctx.fillStyle = "#00FF41"; |
| 7 | ctx.fillRect(10, 10, 100, 50); // x, y, width, height |
info
The coordinate system places (0,0) at the top-left corner. The x-axis increases to the right, and the y-axis increases downward. All drawing operations are expressed in this coordinate space.
Canvas provides two approaches for drawing shapes: convenience methods for simple rectangles, and path-based drawing for arbitrary shapes.
Rectangles are the only shapes with dedicated methods. All other shapes must be drawn using paths.
| 1 | // Fill a rectangle |
| 2 | ctx.fillStyle = "#00FF41"; |
| 3 | ctx.fillRect(50, 50, 200, 100); // x, y, w, h — solid fill |
| 4 | |
| 5 | // Stroke (outline) a rectangle |
| 6 | ctx.strokeStyle = "#FF6B6B"; |
| 7 | ctx.lineWidth = 3; |
| 8 | ctx.strokeRect(50, 200, 200, 100); // x, y, w, h — outline only |
| 9 | |
| 10 | // Clear a rectangular area (transparent) |
| 11 | ctx.clearRect(75, 75, 50, 50); // cuts a hole |
Paths define arbitrary shapes as sequences of connected points. Every path operation follows this pattern: beginPath() to start, drawing commands to define the shape, then fill() or stroke() to render.
| 1 | ctx.beginPath(); |
| 2 | ctx.moveTo(100, 50); // starting point |
| 3 | ctx.lineTo(200, 150); // line to (200,150) |
| 4 | ctx.lineTo(50, 150); // line to (50,150) |
| 5 | ctx.closePath(); // closes back to (100,50) |
| 6 | ctx.fillStyle = "#00FF41"; |
| 7 | ctx.fill(); // fills the triangle |
| 8 | ctx.strokeStyle = "#FFFFFF"; |
| 9 | ctx.lineWidth = 2; |
| 10 | ctx.stroke(); // strokes the outline |
| 11 | |
| 12 | // Arcs and circles |
| 13 | ctx.beginPath(); |
| 14 | ctx.arc(150, 150, 50, 0, Math.PI * 2); // full circle |
| 15 | ctx.fillStyle = "#4FC3F7"; |
| 16 | ctx.fill(); |
| 17 | |
| 18 | // Arc segment (pie slice) |
| 19 | ctx.beginPath(); |
| 20 | ctx.arc(300, 150, 60, 0, Math.PI * 1.5); // 270 degrees |
| 21 | ctx.lineTo(300, 150); |
| 22 | ctx.closePath(); |
| 23 | ctx.fillStyle = "#FFD93D"; |
| 24 | ctx.fill(); |
The arc() method takes: x, y, radius, startAngle, endAngle, and an optional counterclockwise boolean. Angles are measured in radians.
| Method | Parameters | Description |
|---|---|---|
| beginPath() | — | Resets the current path |
| moveTo(x, y) | x, y | Moves pen without drawing |
| lineTo(x, y) | x, y | Draws a straight line |
| arc(x, y, r, s, e) | x, y, radius, start, end | Draws an arc or circle |
| rect(x, y, w, h) | x, y, width, height | Adds a rectangle sub-path |
| closePath() | — | Closes path back to start |
| fill() | — | Fills the current path |
| stroke() | — | Strokes the current path |
Canvas supports solid colors, gradients, and patterns for both fill and stroke operations. The globalAlpha property controls the overall transparency of all drawing operations.
| 1 | // Named, hex, rgb, hsl, or rgba strings |
| 2 | ctx.fillStyle = "#00FF41"; |
| 3 | ctx.fillStyle = "rgb(0, 255, 65)"; |
| 4 | ctx.fillStyle = "rgba(0, 255, 65, 0.5)"; // with alpha |
| 5 | ctx.fillStyle = "hsl(135, 100%, 50%)"; |
| 6 | |
| 7 | // globalAlpha applies to everything drawn after it |
| 8 | ctx.globalAlpha = 0.5; |
| 9 | ctx.fillStyle = "#FF6B6B"; |
| 10 | ctx.fillRect(50, 50, 100, 100); // drawn at 50% opacity |
| 11 | ctx.globalAlpha = 1.0; // reset |
Linear gradients transition colors along a directional line. Radial gradients transition outward from a center point.
| 1 | // Linear gradient |
| 2 | const linearGrad = ctx.createLinearGradient(0, 0, 200, 0); |
| 3 | linearGrad.addColorStop(0, "#00FF41"); |
| 4 | linearGrad.addColorStop(0.5, "#4FC3F7"); |
| 5 | linearGrad.addColorStop(1, "#C084FC"); |
| 6 | ctx.fillStyle = linearGrad; |
| 7 | ctx.fillRect(10, 10, 200, 100); |
| 8 | |
| 9 | // Radial gradient |
| 10 | const radialGrad = ctx.createRadialGradient(150, 150, 10, 150, 150, 100); |
| 11 | radialGrad.addColorStop(0, "#FFD93D"); |
| 12 | radialGrad.addColorStop(1, "#FF6B6B"); |
| 13 | ctx.fillStyle = radialGrad; |
| 14 | ctx.beginPath(); |
| 15 | ctx.arc(150, 150, 100, 0, Math.PI * 2); |
| 16 | ctx.fill(); |
Patterns fill shapes with a repeating image, canvas, or video source.
| 1 | // Pattern from an image |
| 2 | const img = new Image(); |
| 3 | img.src = "tile.png"; |
| 4 | img.onload = () => { |
| 5 | const pattern = ctx.createPattern(img, "repeat"); // repeat, repeat-x, repeat-y, no-repeat |
| 6 | ctx.fillStyle = pattern; |
| 7 | ctx.fillRect(0, 0, canvas.width, canvas.height); |
| 8 | }; |
| 9 | |
| 10 | // Pattern from another canvas |
| 11 | const patternCanvas = document.createElement("canvas"); |
| 12 | patternCanvas.width = 20; |
| 13 | patternCanvas.height = 20; |
| 14 | const pCtx = patternCanvas.getContext("2d"); |
| 15 | pCtx.fillStyle = "#00FF41"; |
| 16 | pCtx.fillRect(0, 0, 10, 10); |
| 17 | const pattern = ctx.createPattern(patternCanvas, "repeat"); |
| 18 | ctx.fillStyle = pattern; |
| 19 | ctx.fillRect(0, 0, 400, 200); |
Canvas provides text rendering similar to CSS, but without automatic line wrapping or layout. You must measure and position text manually.
| 1 | // Configure font before drawing |
| 2 | ctx.font = "bold 24px monospace"; |
| 3 | ctx.textAlign = "left"; // left, right, center, start, end |
| 4 | ctx.textBaseline = "alphabetic"; // top, hanging, middle, alphabetic, ideographic, bottom |
| 5 | ctx.direction = "ltr"; // ltr, rtl, inherit |
| 6 | |
| 7 | // Filled text |
| 8 | ctx.fillStyle = "#00FF41"; |
| 9 | ctx.fillText("Hello, Canvas!", 50, 100); // text, x, y |
| 10 | |
| 11 | // Stroked text (outline only) |
| 12 | ctx.strokeStyle = "#FF6B6B"; |
| 13 | ctx.lineWidth = 1; |
| 14 | ctx.strokeText("Hello, Canvas!", 50, 150); |
| 15 | |
| 16 | // Both fill and stroke |
| 17 | ctx.font = "bold 48px monospace"; |
| 18 | ctx.fillStyle = "#00FF41"; |
| 19 | ctx.fillText("STACK", 50, 220); |
| 20 | ctx.strokeStyle = "#FFFFFF"; |
| 21 | ctx.lineWidth = 1; |
| 22 | ctx.strokeText("STACK", 50, 220); |
| 23 | |
| 24 | // Measure text width |
| 25 | const metrics = ctx.measureText("Hello, Canvas!"); |
| 26 | console.log(metrics.width); // pixel width |
| 27 | console.log(metrics.actualBoundingBoxAscent); |
| 28 | console.log(metrics.actualBoundingBoxDescent); |
info
The drawImage() method has three variants for rendering images, other canvases, and video frames onto the canvas. Pixel-level manipulation enables image filtering and processing.
| 1 | const img = new Image(); |
| 2 | img.src = "photo.jpg"; |
| 3 | img.onload = () => { |
| 4 | // Variant 1: Basic — draw at original size |
| 5 | ctx.drawImage(img, 10, 10); |
| 6 | |
| 7 | // Variant 2: Scaled — specify destination size |
| 8 | ctx.drawImage(img, 10, 10, 200, 150); |
| 9 | |
| 10 | // Variant 3: Source clipping + destination sizing |
| 11 | // (sourceX, sourceY, sourceW, sourceH, destX, destY, destW, destH) |
| 12 | ctx.drawImage(img, 50, 50, 100, 100, 10, 10, 200, 200); |
| 13 | }; |
| 14 | |
| 15 | // Draw from another canvas |
| 16 | ctx.drawImage(offscreenCanvas, 0, 0); |
| 17 | |
| 18 | // Draw current video frame |
| 19 | ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height); |
| 20 | |
| 21 | // Control image smoothing |
| 22 | ctx.imageSmoothingEnabled = false; // nearest-neighbor (pixel art) |
| 23 | ctx.drawImage(img, 0, 0, 400, 400); |
| 24 | ctx.imageSmoothingEnabled = true; // bilinear (default) |
Canvas provides direct access to pixel data for image processing. Each pixel is represented by four values in the Uint8ClampedArray: red, green, blue, and alpha (0–255).
| 1 | // Read pixel data |
| 2 | const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
| 3 | const data = imageData.data; // Uint8ClampedArray |
| 4 | |
| 5 | // Manual grayscale filter |
| 6 | for (let i = 0; i < data.length; i += 4) { |
| 7 | const gray = data[i] * 0.299 + data[i+1] * 0.587 + data[i+2] * 0.114; |
| 8 | data[i] = gray; // red |
| 9 | data[i+1] = gray; // green |
| 10 | data[i+2] = gray; // blue |
| 11 | // data[i+3] — alpha (unchanged) |
| 12 | } |
| 13 | |
| 14 | // Write pixels back |
| 15 | ctx.putImageData(imageData, 0, 0); |
| 16 | |
| 17 | // Create blank ImageData |
| 18 | const newData = ctx.createImageData(100, 100); |
| 19 | |
| 20 | // Invert colors |
| 21 | for (let i = 0; i < data.length; i += 4) { |
| 22 | data[i] = 255 - data[i]; |
| 23 | data[i+1] = 255 - data[i+1]; |
| 24 | data[i+2] = 255 - data[i+2]; |
| 25 | } |
| 26 | ctx.putImageData(imageData, 0, 0); |
warning
Transformations modify the canvas coordinate system before drawing. They accumulate and affect all subsequent operations. Use save() and restore() to isolate transforms.
| 1 | // Save current state (transform + styles) |
| 2 | ctx.save(); |
| 3 | |
| 4 | // Translate — move origin to new position |
| 5 | ctx.translate(100, 50); |
| 6 | |
| 7 | // Rotate — around current origin (in radians) |
| 8 | ctx.rotate(Math.PI / 4); // 45 degrees |
| 9 | |
| 10 | // Scale — stretch coordinates |
| 11 | ctx.scale(2, 1.5); // x2 horizontal, x1.5 vertical |
| 12 | |
| 13 | // Draw rotated/scaled shape |
| 14 | ctx.fillStyle = "#00FF41"; |
| 15 | ctx.fillRect(-25, -25, 50, 50); |
| 16 | |
| 17 | // Restore to pre-transform state |
| 18 | ctx.restore(); |
| 19 | |
| 20 | // Transform matrix — full control |
| 21 | // (a, b, c, d, e, f) |
| 22 | // [a c e] [x] |
| 23 | // [b d f] * [y] |
| 24 | // [0 0 1] [1] |
| 25 | ctx.setTransform(1, 0.5, 0, 1, 50, 50); // skew |
| 26 | ctx.fillRect(0, 0, 100, 100); |
| 27 | |
| 28 | // Reset to identity matrix |
| 29 | ctx.setTransform(1, 0, 0, 1, 0, 0); |
Transformations apply in reverse order: translate → rotate → scale means scale happens first in the local coordinate system, then rotate, then translate to the final position. This is why you typically translate first, then rotate, then draw at (0,0).
Canvas animation uses requestAnimationFrame to create smooth, efficient animations. The browser calls your callback before each repaint (typically 60fps), and provides a high-resolution timestamp for calculating delta time.
| 1 | const canvas = document.getElementById("game"); |
| 2 | const ctx = canvas.getContext("2d"); |
| 3 | |
| 4 | let lastTime = 0; |
| 5 | let x = 100; |
| 6 | let vx = 200; // pixels per second |
| 7 | |
| 8 | function gameLoop(timestamp) { |
| 9 | // Calculate delta time in seconds |
| 10 | const dt = (timestamp - lastTime) / 1000; |
| 11 | lastTime = timestamp; |
| 12 | |
| 13 | // Clear the canvas |
| 14 | ctx.clearRect(0, 0, canvas.width, canvas.height); |
| 15 | |
| 16 | // Update |
| 17 | x += vx * dt; // frame-rate independent movement |
| 18 | if (x > canvas.width - 30 || x < 0) vx = -vx; |
| 19 | |
| 20 | // Draw |
| 21 | ctx.fillStyle = "#00FF41"; |
| 22 | ctx.beginPath(); |
| 23 | ctx.arc(x, canvas.height / 2, 30, 0, Math.PI * 2); |
| 24 | ctx.fill(); |
| 25 | |
| 26 | // Next frame |
| 27 | requestAnimationFrame(gameLoop); |
| 28 | } |
| 29 | |
| 30 | requestAnimationFrame(gameLoop); |
pro tip
Below is a live animation demo using the game loop pattern. The particles move with delta-time-based physics, bounce off walls, and leave a trail effect:
Canvas compositing controls how new drawings blend with existing pixels. The globalCompositeOperation property selects the blending mode.
| Operation | Description |
|---|---|
| source-over | Draw new over old (default) |
| source-atop | Draw new atop old, clip to existing |
| source-in | Show new where old is opaque |
| source-out | Show new where old is transparent |
| destination-over | Draw new behind old |
| destination-atop | Keep old where new is opaque |
| destination-in | Keep old where new is opaque |
| destination-out | Erase old where new is drawn |
| lighter | Sum colors (additive blending) |
| copy | Replace old with new (no blending) |
| xor | Exclusive OR of pixels |
| multiply | Multiply colors (darken) |
| screen | Inverse multiply (lighten) |
| overlay | Multiply or screen based on base |
clip() restricts all subsequent drawing to the current path area. Useful for masks, rounded corners, and scissor effects.
| 1 | // Create a circular clipping region |
| 2 | ctx.beginPath(); |
| 3 | ctx.arc(150, 150, 100, 0, Math.PI * 2); |
| 4 | ctx.clip(); |
| 5 | |
| 6 | // All subsequent drawings are masked to the circle |
| 7 | ctx.fillStyle = "#00FF41"; |
| 8 | ctx.fillRect(0, 0, 300, 300); // only visible inside circle |
| 9 | |
| 10 | // Save/restore to undo clipping |
| 11 | ctx.save(); |
| 12 | ctx.beginPath(); |
| 13 | ctx.rect(50, 50, 200, 200); |
| 14 | ctx.clip(); |
| 15 | // ... clipped operations ... |
| 16 | ctx.restore(); // clipping removed |
Canvas supports drop shadows for all drawing operations. Shadow properties affect everything drawn after they are set.
| 1 | // Configure shadow |
| 2 | ctx.shadowColor = "rgba(0, 0, 0, 0.5)"; |
| 3 | ctx.shadowBlur = 15; |
| 4 | ctx.shadowOffsetX = 5; |
| 5 | ctx.shadowOffsetY = 5; |
| 6 | |
| 7 | // Draw shape with shadow |
| 8 | ctx.fillStyle = "#00FF41"; |
| 9 | ctx.fillRect(50, 50, 100, 100); |
| 10 | |
| 11 | // Text shadow |
| 12 | ctx.font = "bold 48px monospace"; |
| 13 | ctx.shadowBlur = 20; |
| 14 | ctx.shadowColor = "#00FF4180"; |
| 15 | ctx.fillStyle = "#FFFFFF"; |
| 16 | ctx.fillText("GLOW", 50, 200); |
| 17 | |
| 18 | // Disable shadow |
| 19 | ctx.shadowColor = "transparent"; |
best practice
WebGL (Web Graphics Library) provides hardware-accelerated 3D rendering using the GPU. It is based on OpenGL ES and uses shaders written in GLSL. The API is significantly lower-level than Canvas 2D.
| 1 | // Initialize WebGL context |
| 2 | const canvas = document.getElementById("glCanvas"); |
| 3 | const gl = canvas.getContext("webgl"); |
| 4 | if (!gl) { |
| 5 | console.error("WebGL not supported"); |
| 6 | } |
| 7 | |
| 8 | // WebGL uses the GPU via shader programs |
| 9 | // Vertex shader: positions |
| 10 | // Fragment shader: colors |
| 11 | |
| 12 | // Minimal clear-screen example |
| 13 | gl.clearColor(0.05, 0.05, 0.12, 1.0); // dark bg |
| 14 | gl.clear(gl.COLOR_BUFFER_BIT); |
| 15 | |
| 16 | // WebGL2 is also available with more features |
| 17 | const gl2 = canvas.getContext("webgl2"); |
| 18 | |
| 19 | // For most 2D rendering, Canvas 2D is faster to develop |
| 20 | // Use WebGL for: 3D, complex particle systems, game engines |
info
HiDPI / Retina Displays
On high-DPI displays (devicePixelRatio > 1), a CSS-sized canvas will appear blurry. Scale the canvas buffer by devicePixelRatio and scale all drawing coordinates accordingly.
| 1 | function setupHiDPICanvas(canvas, width, height) { |
| 2 | const dpr = window.devicePixelRatio || 1; |
| 3 | canvas.width = width * dpr; |
| 4 | canvas.height = height * dpr; |
| 5 | canvas.style.width = width + "px"; |
| 6 | canvas.style.height = height + "px"; |
| 7 | const ctx = canvas.getContext("2d"); |
| 8 | ctx.scale(dpr, dpr); |
| 9 | return ctx; |
| 10 | } |
| 11 | |
| 12 | const canvas = document.getElementById("myCanvas"); |
| 13 | const ctx = setupHiDPICanvas(canvas, 800, 600); |
| 14 | // Now draw using logical coordinates (800x600) |
| 15 | ctx.fillRect(10, 10, 100, 100); // crisp on retina |
Performance
Canvas performance strategies for complex scenes:
| 1 | // 1. Offscreen Canvas — render in worker threads |
| 2 | const offscreen = canvas.transferControlToOffscreen(); |
| 3 | const worker = new Worker("renderer.js"); |
| 4 | worker.postMessage({ canvas: offscreen }, [offscreen]); |
| 5 | |
| 6 | // 2. Layer canvases — stack multiple canvases |
| 7 | // Static layer: rendered once, never cleared |
| 8 | // Dynamic layer: updated every frame (smaller area) |
| 9 | |
| 10 | // 3. Dirty rectangle tracking — only clear/redraw changed areas |
| 11 | const dirtyRects = []; |
| 12 | function markDirty(x, y, w, h) { |
| 13 | dirtyRects.push({ x, y, w, h }); |
| 14 | } |
| 15 | function renderDirty() { |
| 16 | for (const rect of dirtyRects) { |
| 17 | ctx.clearRect(rect.x, rect.y, rect.w, rect.h); |
| 18 | // redraw only that area |
| 19 | } |
| 20 | dirtyRects.length = 0; |
| 21 | } |
| 22 | |
| 23 | // 4. Batch path operations — minimize state changes |
| 24 | ctx.beginPath(); |
| 25 | for (const p of particles) { |
| 26 | ctx.moveTo(p.x, p.y); |
| 27 | ctx.lineTo(p.x + p.w, p.y + p.h); |
| 28 | } |
| 29 | ctx.stroke(); // one draw call for all lines |
| 30 | |
| 31 | // 5. Avoid shadowBlur in animations |
| 32 | // 6. Use integer coordinates for crisp lines |
| 33 | // 7. Use requestAnimationFrame, not setInterval |
Memory Management
Large canvases and frequent getImageData calls consume significant memory. Always release references when done.
| 1 | // Create temporary offscreen canvases |
| 2 | function createOffscreen(w, h) { |
| 3 | const c = document.createElement("canvas"); |
| 4 | c.width = w; |
| 5 | c.height = h; |
| 6 | return c; |
| 7 | } |
| 8 | |
| 9 | // Pre-render static elements to offscreen canvas |
| 10 | const staticLayer = createOffscreen(400, 300); |
| 11 | // ... render once ... |
| 12 | |
| 13 | // In animation loop, just blit the pre-rendered layer |
| 14 | ctx.drawImage(staticLayer, 0, 0); |
| 15 | // Then draw dynamic elements on top |
best practice