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
Skip the wait
In the first example, replace the
await new Promise(...)line byimg.src = badge.toDataURL();only. The badge is not drawn (the image decodes a moment later): the stretched crop and the inverted corner both read0,0,0,0, and there is no error.await img.decode()after settingsrcis the shorter way to wait. - 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 prints0,0,0,0.
Code & diagrams
Edge cases: an image that has not loaded, a read outside the canvas, and a read of width 0.
Preview
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.// 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.Each row is one setting. Reading pixels proves what each rule does.
Preview
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,255Break 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).
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.
- ▸
getImageDatais slow on a GPU-backed canvas, because the pixels must be copied to the CPU. UsegetContext("2d", { willReadFrequently: true })if you read often, and read as small an area as you can. - ▸
destination-outis how an eraser works and how a mask cuts a hole, andmultiplyis how heat layers are tinted: pick the operation for the job instead of calculating colours.
Remember this
- 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
**
drawImagehas 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 anImageBitmap. 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) throwsInvalidStateError: … The HTMLImageElement provided is in the 'broken' state.in Chromium. Always wait foronload(orawait img.decode()). - 3
Pixels are four numbers each.
getImageData(x, y, w, h)returns anImageDatawhosedatais a flat listr, 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 callputImageData(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 throwsIndexSizeError. - 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,toDataURLandtoBlobthrowSecurityError, 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 sendsAccess-Control-Allow-Origin, andimg.crossOrigin = "anonymous"set beforesrc. If you setcrossOriginbut the server sends no such header, the image does not load at all:onerrorfires and the console shows the CORS message (both cases are in the measured block). - 5
Compositing decides how new paint meets old.
globalAlphamakes everything you paint partly transparent.globalCompositeOperationpicks the rule:source-over(the default, new on top),destination-over(new behind),destination-out(new paint erases what it covers),multiplyandlighter(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
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 untilrestore()(Topic 11.3), so wrap it insave()/restore()or all later drawing disappears.
Explain it without notes
Why does drawImage sometimes paint nothing and show no error?
What is in ImageData.data and how long is it for a 10 × 5 area?
What makes a canvas tainted, what stops working, and how do you avoid it?
Practice
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).
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
drawImageand I wait for the image to load.I can read, change and write pixels with
getImageDataandputImageData.I can say what taints a canvas and how to avoid the
SecurityError.I can choose between
source-over,destination-outandmultiply, and I wrapclip()insave()/restore().