A layered raster canvas where a stroke lands under the pointer at every zoom, undo really steps back, and the export is the canvas or a named enlargement of it.
Initializing in your browser…
Six published edge operators (Sobel, Prewitt, Scharr, Roberts and both Laplacians) plus the real four-stage Canny, each checked against the browser's own feConvolveMatrix and each naming the paper it comes from.
Mark a picture up with arrows, shapes, text and freehand, in the picture's own pixels: an annotation is the same size in the file whatever the view is zoomed to.
Render SVG to PNG, JPG or WebP at a chosen size, with the size worked out from the file by the rules that define it and checked against the browser's own rendering.
One short stroke drawn with the pointer over the exact middle of a 1280 by 720 canvas, then exported.
Input
pointer over canvas pixel (640, 360) · brush · export PNG
Where the ink is in the file
before after at 100 percent zoom ink centred on (640, 471) (639, 360) out by 111 px 0 px at 50 percent zoom ink centred on no ink (639, 360) undo after two strokes ink pixels left 472 236
The pointer was converted against the CONTAINER, which centres the canvas inside it and scales it about its own middle, so a stroke landed 111 px below the pointer at 100 percent and missed the canvas entirely at 50. The conversion now goes through the canvas element’s own bounding rectangle, which is where the element really is whatever transform is on it. Undo was reading one place past the end of its history, so it always restored the picture already on screen.
A layered raster canvas for sketching in the browser: pen, brush, eraser, five shapes, flood fill, eyedropper and text, with real layers and real undo. The ink lands under the pointer at every zoom, and the export is the canvas at its own size or a named enlargement of it.
Sketch layout ideas or wireframes without opening a design application.
Draw shapes and label them, then export at 2x for a high-resolution screen.
A lightweight space for sketching, with layers when you want them.
Make basic icons or illustrations with transparency preserved in the PNG.
Zoom to 50 percent and draw across the middle of the canvas. The ink in the exported PNG is centred on canvas pixel (639, 360), which is where the pointer was. Before this pass the same stroke put no ink in the file at all.
Draw on a new layer with the Brush, then switch to the Eraser to cut shapes out; the eraser uses destination-out so the holes are truly transparent over the checkerboard. Export as PNG to keep them.
Put a base illustration on the Background layer and shading on a new one, then drop that layer’s opacity to blend it in. The flattened result exports as a single file.
A drawing tool makes one promise before any other: the ink appears under the pointer. This version keeps it at every zoom, which the previous one did not.
**A stroke landed 111 px below the pointer.** The pointer position was converted against the canvas’s CONTAINER, with the pan offset subtracted and the zoom divided out. The container centres the canvas inside itself and scales it about its own middle, and neither of those is in that arithmetic. Measured on 2026-09-05 on the default 1280 by 720 canvas: a stroke drawn with the pointer over the exact centre came out of the export centred on (640, 471), 111 px too low. At 50 percent zoom there was no ink in the exported file at all, because the stroke had been placed off the canvas entirely. The conversion now goes through the canvas element’s OWN bounding rectangle and the ratio between that box and its backing store, which is where the element really is on screen whatever transform is applied to it. Measured after: (639, 360) at 100 percent, at 50 and at 200.
**Undo restored the picture already on screen.** The history array and the position in it were two separate pieces of state, and the position was incremented rather than derived from the array. React runs an effect twice in development, so the initial save ran twice: the array updater is pure and produced one entry while the counter ran twice and produced 1, leaving the position permanently one past the end. `history[index - 1]` was therefore the state just saved. Measured: two strokes, 472 ink pixels, undo, still 472. The two are one piece of state now, updated together, and an undo takes the count back to 236 and a second one to zero.
Undo also used to be a race of its own: the restored layers were set immediately and each one’s image decoded later, so an export taken straight after an undo could read a canvas that was half restored. The restore now waits for every layer before it commits.
**The export says what it is.** It is the flattened composite at the canvas’s own 1280 by 720, or a whole multiple of it: a raster drawing has no resolution above the one it was drawn at, so 2x and 3x are ENLARGEMENTS and the page names the filter, Lanczos 3 from this project’s resampler, the same one the resizer and the upscaler use. A 2x export scaled back down is the 1x export on every pixel. The format is read back off the produced blob rather than assumed, because a canvas asked for a type it cannot write returns a PNG and says nothing, and the quality argument is only passed where quality does something.
The rest is what it was, and it is good. Pen and Brush run on perfect-freehand’s `getStroke` with thinning 0.5 and 0.7, so the Brush tapers more sharply with speed; pressure comes through the Pointer Events API and falls back to a constant on a mouse. The Eraser is a real eraser: it strokes with `destination-out`, cutting genuine transparency rather than painting white. Five geometric tools draw live on a separate preview canvas and are baked into the active layer only on release, with independent Fill and Stroke toggles. The Fill tool is a stack-based flood fill with a tolerance of 32 per channel, the Eyedropper samples the composite, and the Text tool stamps a string in one of five families at 8 to 200 px. The document is layered, each layer its own offscreen canvas with visibility, lock and opacity, over a checkerboard that shows what is transparent. The canvas zooms from 10 to 400 percent with a 20 px grid, and everything runs in the browser with nothing uploaded.
Yes, at every zoom, and it is asserted in the test suite by drawing at four known points and finding the marks in the exported file. Before this pass the conversion was measured against the container rather than the canvas, so a stroke landed 111 px below the pointer at 100 percent zoom and missed the canvas entirely at 50.
Yes. The history and the position in it are one piece of state now. They were two, and the position was one past the end for the whole session, so every undo restored the picture that was already on screen.
It enlarges the drawing to twice its size with Lanczos 3 and says so. A raster drawing has no detail above the resolution it was drawn at, so 2x is an enlargement rather than a re-render, and the page states that rather than implying otherwise.
Not here: this canvas starts blank. To mark up a photograph or a screenshot, use the Image Annotator, which loads your picture as the background.
PNG, JPG and WebP. PNG keeps transparency; JPG flattens it. The type is read back off the produced bytes and the file is named after what it actually is.
Images are decoded, edited, and exported entirely inside this browser tab. No originals, exports, or metadata are uploaded.