A Paint Program
A paint program you can use on this page, built from an engine that never sees a mouse. Points go in and pixels come out, so every brush, every layer and every undo is checked by tests that draw with no window. The browser build is about 41 KB of WebAssembly, and before the site ships it, it must draw a test picture with exactly the pixels the native tests pinned.
The engine is plain Zig built for wasm32-freestanding, so CI builds the file this page runs and nothing is committed. It is a teaching program: one brush engine, two layers, and 8-bit colour blended the way a browser canvas blends it.
- One mark per pointer event makes a stroke that depends on hand speed. Walk the path and carry the leftover distance into the next segment, and the spacing stays even.
- A stroke at 50% opacity must stay at 50% where it crosses itself. Build its coverage in a separate buffer and apply the opacity once, at the end.
- Undo does not need a copy of the picture. Keep the rectangle an action changed, and undo and redo become the same swap.
- Running out of memory should cost an undo step, not the stroke. In Zig that is an ordinary error you can decide what to do with.
13 chapters.
- A Paint Program You Can TestA paint program you can use on this page, and the one rule that lets tests check every brush.
- StrokesPointer samples arrive at uneven distances. Walk the path between them, place a dab every few pixels, and carry the leftover distance into the next segment.
- Smoothing, Pressure and ScatterSteady a shaky hand with a moving average, size each dab from pen pressure, and scatter dabs with a random generator that repeats exactly.
- The TipA dab is a disc with a fading edge. Squash it into an ellipse, turn it, and the same twenty lines draw a soft brush, a calligraphy nib and a ribbon.
- Bristles and PaperA row of thin hairs across the stroke gives separate tracks. A fixed grid of paper heights decides where a dry brush touches.
- Oil: Pickup and Paint LoadAn oil brush carries paint. It picks up some of the colour it is dragged through, and it runs dry as it travels.
- Flow and OpacityDabs overlap. Build a stroke's coverage in its own buffer, apply its opacity once at the end, and a stroke at 50% stays at 50% where it crosses itself.
- Layers and the EraserLayers are transparent pixel arrays drawn over white paper. Store them premultiplied, and the eraser becomes one multiply.
- UndoKeep only the rectangle an action changed. Undo and redo are the same swap, two stacks are one array and an index, and running out of memory costs an undo step, not the stroke.
- FillA flood fill finds its region a few pixels at a time. A queue and a visited map do it in fixed memory, where recursion would run out of stack.
- SavingThe picture flattened onto white paper and written as a PNG. Each row gets whichever of the five filters makes its bytes smallest.
- WatercolourWater and pigment on a grid of paper. They spread to neighbouring cells, the paper dries, and pigment settles into it. Fixed steps keep the run the same on every machine.
- A Native WindowThe same engine in a desktop window with raylib. Windows builds from Linux with one flag. macOS does not, and the reason is a licence, not the compiler.