Command Palette

Search for a command to run...

Hectal
PHASE 11Intermediate ~22 min· topic 5 of 7

Topic 11.5

Images, Pixels and Compositing

In one line

A canvas can draw images, read and write its own pixels, mix new paint with old in many ways, and clip drawing to a shape. Reading pixels of an image from another site is blocked, and the error is the one to know.

Think of it like this

A collage table. You can paste a photo (or a cut-out of one), you can look at one square millimetre of the picture, you can paint a square in colour, and you can lay a stencil over the table so paint only reaches the stencil's hole. Canvas does all four.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

ImageData
An object holding a grid of pixels as a flat list of numbers: red, green, blue, alpha for each pixel.
alpha
How opaque a pixel is: 0 is invisible, 255 is solid.
origin
The site an image or page comes from: its scheme (https), host and port together. If any of the three differs, it is another origin.
tainted
A canvas that has drawn another origin's image without permission: it can be shown but not read.
compositing
The rule for mixing new paint with the pixels already there.

Step by step

01Draw an image only when it has loaded

The first example creates a small badge on one canvas, turns it into an image with toDataURL, and waits for onload. Before the wait it tries drawImage on an empty image: no error, alpha 0. After the wait the badge paints, then the middle 20 × 20 is cropped and stretched to 100 × 100.

02Read, change and write pixels

It reads the 40 × 40 badge (1,600 pixels, 6,400 numbers), flips 255 - value for red, green and blue, and writes it back at x = 200. The yellow centre becomes blue and the teal border a pink. Alpha stays.

03Compare compositing rules

The second example prints, for six settings, the colour of the overlap of two squares, of the new square alone, and of the old square alone. destination-out leaves the overlap 0,0,0,0: the new square erased it. multiply darkens it. lighter adds the colours and gets close to white.

04Clip, then restore

A wide dark bar is painted inside a clip circle. The pixel inside the circle is dark, the same bar outside is transparent. After restore(), a yellow rectangle shows fully: the clip is gone.

Try it yourself

  1. 1

    Skip the wait

    In the first example, replace the await new Promise(...) line by img.src = badge.toDataURL(); only. The badge is not drawn (the image decodes a moment later): the stretched crop and the inverted corner both read 0,0,0,0, and there is no error. await img.decode() after setting src is the shorter way to wait.

  2. 2

    Forget the restore

    In the third example delete the ctx.restore(); after the clip log. The yellow rectangle at (120, 90) is outside the clip circle, so it disappears and the last log line prints 0,0,0,0.

Code & diagrams

drawImage, getImageData and putImageData New tab

Edge cases: an image that has not loaded, a read outside the canvas, and a read of width 0.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

drawImage before the image loaded: no error, pixel alpha 0
middle of the stretched crop: 233,196,106,255
pixels: 1600 numbers: 6400
inverted corner: 213,98,112,255
outside the canvas: 0,0,0,0
IndexSizeError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The source width is 0.
A tainted canvas (measured in Chromium, not live: the preview has no other origin)text
// Case 1: a page on http://localhost:4100 draws an image from http://localhost:4101 (no CORS header, no crossOrigin set).
// The image shows, but the canvas is tainted:
ctx.getImageData(0, 0, 1, 1)  ->  SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.
canvas.toDataURL()            ->  SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
canvas.toBlob(...)            ->  SecurityError: Failed to execute 'toBlob' on 'HTMLCanvasElement': Tainted canvases may not be exported.

// Case 2: the same, but img.crossOrigin = "anonymous" is set and the server still sends no header.
// The image does not load at all (img.onerror fires, nothing to draw), and the console says:
Access to image at 'http://localhost:4101/a.png' from origin 'http://localhost:4100' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Compositing and clipping New tab

Each row is one setting. Reading pixels proves what each rule does.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

source-over: overlap: 231,111,81,255 | only new: 231,111,81,255 | only old: 42,157,143,255
globalAlpha 0.5: overlap: 137,134,112,255 | only new: 231,112,82,128 | only old: 42,157,143,255
destination-out: overlap: 0,0,0,0 | only new: 0,0,0,0 | only old: 42,157,143,255
multiply: overlap: 38,69,46,255 | only new: 231,111,81,255 | only old: 42,157,143,255
lighter: overlap: 255,255,224,255 | only new: 231,111,81,255 | only old: 42,157,143,255
destination-over: overlap: 42,157,143,255 | only new: 231,111,81,255 | only old: 42,157,143,255
clip: inside the circle 38,70,83,255 | same bar, outside the circle 0,0,0,0
after restore, drawing is not clipped: 233,196,106,255

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Read a canvas that drew another site's image

In your own page, draw new Image() with src = "https://example.com/a.png" (a server with no CORS header and no crossOrigin set), then call ctx.getImageData(0, 0, 1, 1).

terminal
$ Console
── what you'll see ──
SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.

Myth vs fact

Myth

drawImage throws if the image is not ready.

Fact

It silently paints nothing for an image that is not loaded yet. Only a broken image throws.

Myth

getImageData always works on a canvas I made.

Fact

One cross-origin image taints it for good. You must start a fresh canvas.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    getImageData is slow on a GPU-backed canvas, because the pixels must be copied to the CPU. Use getContext("2d", { willReadFrequently: true }) if you read often, and read as small an area as you can.

  • ▸

    destination-out is how an eraser works and how a mask cuts a hole, and multiply is how heat layers are tinted: pick the operation for the job instead of calculating colours.

Remember this

  1. 1

    The problem this solves: a dashboard sometimes needs a logo, a map tile or a heat layer, and sometimes needs to know the colour under the pointer (Topic 12.3 uses it for clicking). These are image and pixel jobs, not path jobs.

  2. 2

    **drawImage has three shapes.** drawImage(img, x, y) paints the image at its size. drawImage(img, x, y, w, h) stretches it. The nine-argument form crops a part of the image (sx, sy, sw, sh) and paints it into a rectangle (dx, dy, dw, dh). The source can be an <img>, another <canvas>, a <video> frame or an ImageBitmap. An image that has not loaded yet paints nothing and raises no error: the first example proves it. A broken image (one that failed to load) throws InvalidStateError: … The HTMLImageElement provided is in the 'broken' state. in Chromium. Always wait for onload (or await img.decode()).

  3. 3

    Pixels are four numbers each. getImageData(x, y, w, h) returns an ImageData whose data is a flat list r, g, b, a, r, g, b, a…, 0 to 255 each, row by row. A 40 × 40 area holds 1,600 pixels and 6,400 numbers. Change the numbers and call putImageData(image, x, y) to write them back (the example inverts the colours of the badge). Reading outside the canvas gives transparent black, and a width or height of 0 throws IndexSizeError.

  4. 4

    A tainted canvas refuses to be read. Drawing an image from another origin (another site) without permission taints the canvas. Drawing still works, but getImageData, toDataURL and toBlob throw SecurityError, to protect private pictures. The preview has no other site, so the block below is measured in real Chromium, not live. The cure is a server that sends Access-Control-Allow-Origin, and img.crossOrigin = "anonymous" set before src. If you set crossOrigin but the server sends no such header, the image does not load at all: onerror fires and the console shows the CORS message (both cases are in the measured block).

  5. 5

    Compositing decides how new paint meets old. globalAlpha makes everything you paint partly transparent. globalCompositeOperation picks the rule: source-over (the default, new on top), destination-over (new behind), destination-out (new paint erases what it covers), multiply and lighter (mixing colours). The second example paints the same teal square and orange square with six settings and prints three pixels each: the overlap, only the new, only the old.

  6. 6

    Clipping is a stencil. clip() turns the current path into a stencil: later drawing shows only inside it. The wide bar in the example only shows inside a circle. A clip stays until restore() (Topic 11.3), so wrap it in save()/restore() or all later drawing disappears.

Explain it without notes

01

Why does drawImage sometimes paint nothing and show no error?

02

What is in ImageData.data and how long is it for a 10 × 5 area?

03

What makes a canvas tainted, what stops working, and how do you avoid it?

Practice

01

Write grey(ctx, x, y, w, h) that turns an area to greyscale with getImageData and putImageData (use the average of red, green and blue).

02

Paint a wide teal rectangle that only shows inside a circle of radius 30, using clip, and make sure later drawing is not clipped.

Trade-offs

  • ↔

    Pixel work is powerful (filters, picking, heat layers) but reads and writes four numbers per pixel, which is slow for large areas.

Done when you can

  • I can use all three forms of drawImage and I wait for the image to load.

  • I can read, change and write pixels with getImageData and putImageData.

  • I can say what taints a canvas and how to avoid the SecurityError.

  • I can choose between source-over, destination-out and multiply, and I wrap clip() in save()/restore().