cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Drawing with canvas

Draw shapes, pixels, and animations with the canvas 2D context, including paths, ImageData, high-DPI sizing, animation loops, and OffscreenCanvas.

By the end, you can
  • 01
    Draw with the 2D contextChoose the right canvas size, context settings, shapes, paths, text, and high-DPI scaling.
  • 02
    Work with pixelsRead and write ImageData, apply filters, and explain why cross-origin images can taint a canvas.
  • 03
    Animate responsiblyUse requestAnimationFrame, clear each frame, respect reduced motion, and feature-detect OffscreenCanvas.

Canvas as pixels, not page objects

A <canvas> is a rectangular drawing surface. JavaScript asks a drawing context to paint pixels into that surface: rectangles, lines, text, images, gradients, and raw pixel data. That makes canvas excellent for games, paint tools, visual effects, audio visualizers, and dense diagrams that redraw often.

The important mental model is immediate mode. When you call ctx.fillRect(20, 20, 80, 40), the browser changes pixels. It does not keep a rectangle object you can later select from the DOM and move. If you want to move that rectangle, you keep your own data model, clear the canvas, and draw the scene again.

Real-life analogyCanvas is paint on a wall

Imagine painting a circle directly onto a wall. Once the paint dries, there is no separate “circle object” to pick up. The DOM and SVG are more like paper cutouts pinned to a board: the browser remembers each cutout, so you can grab one by its element and move it.

In real life: Paint brushed onto a wall
In JavaScript: Canvas pixels after fillRect()
In real life: Paper cutouts pinned to a board
In JavaScript: DOM or SVG elements the browser keeps
In real life: To move paint, repaint the wall
In JavaScript: To move canvas content, clear and redraw
In real life: A sketchbook of planned shapes
In JavaScript: Your own JavaScript scene data

Where the analogy stops: A real wall keeps paint for years. Canvas pixels disappear when you resize the drawing buffer, clear it, or redraw over them. Also, browsers may store pixels in GPU memory rather than a literal wall-like bitmap.

The short definition

Canvas is a browser API for drawing into a bitmap. You control it with JavaScript, usually through the CanvasRenderingContext2D returned by canvas.getContext("2d").

This lesson stays on the 2D context. WebGL and WebGPU are separate APIs for GPU graphics; they use the same canvas element but a very different programming model.

The 2D context and the drawing buffer

INTERACTIVE

Start with two objects: the canvas element and its 2D context. The element is part of the page. The context is the painter’s toolbox: it remembers settings such as fillStyle, strokeStyle, lineWidth, transforms, clipping, and the current path.

Get the 2D contextPop out in the code editor (opens in a new tab)JavaScript
const canvas = document.querySelector("canvas");const ctx = canvas.getContext("2d"); ctx.fillStyle = "royalblue";ctx.fillRect(10, 10, 80, 40);ctx.clearRect(0, 0, canvas.width, canvas.height);

Two sizes matter. The width and height attributes set the drawing buffer. If you omit them, the buffer is 300×150. CSS width and height only stretch that buffer on the page. On high-density screens, stretching a small buffer can look blurry, so many apps multiply the buffer by devicePixelRatio and scale the context back to CSS pixels.

High-DPI sizing helperPop out in the code editor (opens in a new tab)JavaScript
function resizeCanvas(canvas, cssWidth, cssHeight, dpr) {  canvas.style.width = cssWidth + "px";  canvas.style.height = cssHeight + "px";  canvas.width = Math.round(cssWidth * dpr);  canvas.height = Math.round(cssHeight * dpr);  const ctx = canvas.getContext("2d");  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);  return ctx;} // On a DPR 2 screen: 600×300 backing pixels.

On a device pixel ratio of 2, a 300×150 CSS canvas should usually have a 600×300 backing store. The drawing code can still use coordinates like 300 and 150 because setTransform(2, 0, 0, 2, 0, 0) maps CSS-sized coordinates to device pixels.

Shapes lab: paint pixels on the canvas
Shape commandsPop out in the code editor (opens in a new tab)JavaScript
const canvas = document.querySelector("canvas");const ctx = canvas.getContext("2d");ctx.fillStyle = "#5b8def";ctx.strokeStyle = "#1f2937";ctx.lineWidth = 4;ctx.fillRect(24, 24, 96, 56);ctx.strokeRect(148, 24, 96, 56);ctx.beginPath();ctx.arc(92, 142, 36, 0, Math.PI * 2);ctx.fill();ctx.beginPath();ctx.moveTo(170, 164);ctx.lineTo(214, 106);ctx.lineTo(258, 164);ctx.closePath();ctx.stroke();ctx.fillText("Canvas", 24, 210);
Canvas outputscaled backing store
Try it yourself

Drawings appear after hydration, because canvas pixels are a browser-only effect.

Toggle drawing commands and high-DPI sizing. Width and height attributes set the drawing buffer; CSS only changes display size.

Try switching the high-DPI toggle. The code did not change the shapes; it changed the buffer math. That is the same idea you learned in Sizes, scrolling & coordinates: browser coordinates are often CSS pixels, while physical device pixels can be denser.

Paths and shapes: sketch first, ink later

STEP THROUGH

Canvas has immediate drawing methods such as fillRect(), but paths are different. A path is a temporary sketch stored by the context. beginPath() starts a new sketch, moveTo() moves the pencil, lineTo() and arc() add segments, and fill() or stroke() finally paints pixels.

Real-life analogyA path is a pencil sketch before inking

Think of a comic artist blocking out a panel in pencil. The sketch tells the artist where the final ink will go. Until it is inked, the reader sees nothing. Canvas paths behave the same way.

In real life: Lift the pencil and move to a spot
In JavaScript: moveTo(x, y)
In real life: Sketch a line or curve lightly
In JavaScript: lineTo() and arc()
In real life: Connect back to the start
In JavaScript: closePath()
In real life: Ink the sketch
In JavaScript: stroke() or fill()

Where the analogy stops: A pencil mark is visible immediately. A canvas path is not visible until a painting command inks it.

Step through a path before it becomes pixels
Step 0 of 6Ready
Your turn: follow the blue line

A path is a sketch held by the 2D context. Step through the sketch, then ink it with fill or stroke.

Running in
  1. script
Next: line 2
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const ctx = canvas.getContext("2d");ctx.moveTo(52, 124);ctx.lineTo(138, 42);ctx.arc(194, 124, 42, Math.PI, 0);ctx.closePath();ctx.fill();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Change the line that inks the path

Changing a setting creates a fresh recording. The preview uses the same pure path model.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Live path previewclosed · fill

Angles in arc() are radians, not degrees. A full circle is Math.PI * 2. Half a circle is Math.PI. If degrees are friendlier, convert with degrees * Math.PI / 180 before passing the angle.

Fill rules: nonzero and evenodd

fill() uses the nonzero fill rule by default. You can pass "evenodd" for donut-like paths where overlapping rings cut holes. The rule only matters for filled paths with overlaps or nested subpaths; plain rectangles and circles look the same.

Images and pixels: ImageData is a mosaic

INTERACTIVE

Canvas can draw images, video frames, gradients, and generated art. Once pixels are in the buffer, getImageData() gives you an ImageData object. Its data property is a Uint8ClampedArray: a flat list of bytes, four per pixel: red, green, blue, alpha.

Real-life analogyImageData is a mosaic of numbered tiles

Imagine a mosaic where every tile has four tiny knobs: red, green, blue, and alpha. Filters are just rules for turning those knobs. Inverting a picture changes each color channel to 255 - value.

In real life: One tiny tile
In JavaScript: One pixel
In real life: Four labels on the tile
In JavaScript: Red, green, blue, alpha
In real life: Numbers from 0 to 255
In JavaScript: Byte values in Uint8ClampedArray
In real life: Rebuild the mosaic
In JavaScript: putImageData(image, 0, 0)

Where the analogy stops: Real mosaic tiles have one color each. Canvas pixels store separate color channels, and alpha controls transparency.

Pixels lab: edit the mosaic tiles
Pixel filter loopPop out in the code editor (opens in a new tab)JavaScript
const image = ctx.getImageData(0, 0, canvas.width, canvas.height);for (let i = 0; i < image.data.length; i += 4) {  const red = image.data[i];  const green = image.data[i + 1];  const blue = image.data[i + 2];  image.data[i] = 255 - red;  image.data[i + 1] = 255 - green;  image.data[i + 2] = 255 - blue;}ctx.putImageData(image, 0, 0);
Generated pictureinvert
Try it yourself

Click a filter to read ImageData, edit bytes, and put it back. Move over the canvas to inspect one RGBA tile.

The picture is generated in this page. If you draw an unapproved cross-origin image first, getImageData throws SecurityError instead of returning pixels.

The lab generates its picture in the page. If you draw an image from another origin without permission and then call getImageData(), the canvas becomes tainted and the browser throws SecurityError. That protects private pixels, such as images from a site where the visitor is logged in.

An animation loop: clear and redraw

INTERACTIVE

A canvas animation is usually a loop: measure time, update your own model, clear the old pixels, draw the new frame, and schedule the next frame with requestAnimationFrame(). You saw the timing side in Timers and the Event loop; canvas adds the redraw step.

Animation loop: clear, update, draw, request again
Bouncing ball loopPop out in the code editor (opens in a new tab)JavaScript
let x = 40;let velocity = 180;let lastTime = performance.now(); function frame(now) {  const seconds = (now - lastTime) / 1000;  lastTime = now;  x = x + velocity * seconds;  if (x > 260 || x < 40) velocity = -velocity;  ctx.clearRect(0, 0, 300, 160);  ctx.beginPath();  ctx.arc(x, 80, 22, 0, Math.PI * 2);  ctx.fill();  requestAnimationFrame(frame);}
Canvas animationpaused
Try it yourself

Motion is available, but still starts only after you press Play. Press Step once or Play. This lesson never autostarts motion.

The loop uses requestAnimationFrame. It clears the old pixels every frame instead of leaving a trail.

Notice that the lab does not autostart. Motion should be a choice, especially for readers who prefer reduced motion. The component reads prefers-reduced-motion after mount, offers a Step button, and clears its animation frame on unmount.

OffscreenCanvas: move drawing work to a worker

FEATURE DETECT

OffscreenCanvas lets some drawing happen away from the main page thread, often inside a Worker. It is useful when drawing is heavy enough to compete with clicks, typing, or scrolling. Support is broad in modern browsers, but not universal, so feature detection is part of the API shape.

OffscreenCanvas: draw from a worker when available
Feature-detected transferPop out in the code editor (opens in a new tab)JavaScript
if ("OffscreenCanvas" in window) {  const offscreen = canvas.transferControlToOffscreen();  const worker = new Worker(workerUrl);  worker.postMessage({ canvas: offscreen }, [offscreen]);} else {  showMessage("This browser does not support OffscreenCanvas yet.");}
Worker canvasfeature-detected
Try it yourself

Press Try worker drawing to feature-detect OffscreenCanvas in this browser.

OffscreenCanvas support varies. Feature-detect first and always keep a main-thread or static fallback.

The demo creates a Worker from a Blob URL only after you click. Reset terminates the worker and revokes the URL. That cleanup matters: workers keep running until you stop them, and transferred canvases cannot be drawn from the main thread afterward.

Choose canvas, SVG, or DOM for the job

SORTER

Canvas is powerful, but it is not a replacement for HTML. If users need to focus, select, search, copy, or hear content with assistive technology, real DOM elements usually win. SVG sits between them: graphical like canvas, but retained as elements.

Canvas, SVG, and DOM compared
ThingCanvasSVGDOM
Memory modelPixels in a drawing bufferElements for each shapeElements for document UI
Best atGames, paint tools, dense visualizationsDiagrams and charts with real shapesForms, text, app layout
Hit testingYou write math or use a hidden modelBrowser can target each shapeBrowser targets elements
AccessibilityYou provide separate labels or controlsShapes can be labelledBuilt-in for semantic elements
Which rendering tool fits?
  • A brush app with thousands of strokes
  • A line chart where each point needs a tooltip and keyboard focus
  • A checkout form with inputs, labels, and validation messages
  • A fast sprite game redrawn 60 times a second
  • A scalable logo that should stay sharp at any size
  • A paragraph with links, selection, and browser find support
Try it yourself
0 of 6 correct

Sort each real task into canvas, SVG, or DOM.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Where you’ll use this in real projects

A practical canvas component normally keeps a plain JavaScript model beside the pixels. For a diagram, that might be an array of nodes and edges. For a game, it might be sprites with positions and velocities. For a signature pad, it might be pointer strokes. The canvas is the output, not the source of truth.

Keep a scene model beside the canvasPop out in the code editor (opens in a new tab)JavaScript
const circles = [  { x: 40, y: 50, radius: 18, color: "tomato" },  { x: 120, y: 80, radius: 28, color: "royalblue" },]; function drawScene(ctx) {  ctx.clearRect(0, 0, 300, 150);  for (const circle of circles) {    ctx.fillStyle = circle.color;    ctx.beginPath();    ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);    ctx.fill();  }}

Hit testing uses the same model. Canvas will not tell you which circle was clicked; you compare the pointer coordinate with your stored shapes. A rectangle check is a bounds comparison, and a circle check is distance from the center.

Common misconceptions

  • “Canvas shapes are DOM nodes.” They are pixels. Keep your own model if you need to move, delete, label, or hit-test drawn things.
  • “CSS size and canvas size are the same.” The attributes set the drawing buffer. CSS only stretches the result.
  • “Paths draw as soon as you call lineTo().” Path commands sketch. stroke() or fill() inks.
  • “Pixel filters can read any image.” Cross-origin images without permission taint the canvas, and getImageData() throws SecurityError.
  • “Canvas animation means never clearing.” Most loops clear and redraw each frame. Trails are an effect you choose deliberately.
  • “OffscreenCanvas is automatic performance.” It can move drawing work, but you still pay for messages, data copies, fallbacks, and cleanup.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the radians conversion

Before running it, predict the console output.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const degrees = 180;
const radians = degrees * Math.PI / 180;
console.log(radians === Math.PI);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeChoose the high-DPI buffer

    Work out the backing store size for a 300×150 canvas on a DPR 2 screen.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function backing(cssWidth, cssHeight, dpr) {
      return [cssWidth * dpr, cssHeight * dpr].join("x");
    }
    console.log(backing(300, 150, 2));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeInvert one pixel

      Predict the exact comma-separated RGBA values.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const pixel = [10, 20, 30, 255];
      console.log([255 - pixel[0], 255 - pixel[1], 255 - pixel[2], pixel[3]].join(","));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the bug: the path never appears

        The code runs without an error, but the canvas stays blank. Explain the missing line and add it.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        ctx.beginPath();
        ctx.moveTo(20, 20);
        ctx.lineTo(120, 20);
        ctx.lineTo(120, 80);
        // Why is nothing visible?
          Exercise 5 · ChallengePlan a click target model

          Write the hit-test logic you would use after converting a pointer event to canvas coordinates.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const buttons = [
            { x: 20, y: 20, width: 90, height: 36, label: "Save" },
            { x: 120, y: 20, width: 90, height: 36, label: "Cancel" },
          ];
          
          function hitButton(point) {
            // return the clicked button, or undefined
          }

            Check your understanding

            7 QUESTIONS
            Canvas quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What is the canvas element’s default drawing buffer size when you omit width and height attributes?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this angle code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log(Math.PI === 180 * Math.PI / 180);

              Choose an answer to see the explanation.

            3. Question 3 of 7Which statement about paths is accurate?

              Choose an answer to see the explanation.

            4. Question 4 of 7Which type stores ImageData.data values?

              Choose an answer to see the explanation.

            5. Question 5 of 7What can make getImageData throw SecurityError?

              Choose an answer to see the explanation.

            6. Question 6 of 7What is the usual animation loop pattern?

              Choose an answer to see the explanation.

            7. Question 7 of 7What is the safest way to use OffscreenCanvas?

              Choose an answer to see the explanation.

            Key takeaways

            • Canvas is immediate-mode drawing: calls change pixels, not DOM objects.
            • The 2D context stores drawing settings, the current path, and transforms; save() and restore() let you put that tray of settings aside and pick it back up.
            • Width and height attributes set the buffer. CSS size only stretches it, so high-DPI screens often need a larger backing store.
            • Paths are sketches until fill() or stroke() paints them. Angles are radians.
            • ImageData.data is a Uint8ClampedArray of RGBA bytes. Tainted canvases cannot be read.
            • Animations clear and redraw in requestAnimationFrame(); OffscreenCanvas is useful but must be feature-detected.

            Canvas is JavaScript-controlled pixel drawing: keep your own scene model, then paint the current frame into a bitmap.

            Up next: Files & Blobs.

            CompleteFrontend Clear concepts. Working examples.