webpost.ing

How webpaint.ing is built: the graphics card and the tools

By releases ·

webpaint.ing is the drawing app that sits beside this site. Earlier notes said what the graphics card does for it and how shared drawings stay in step. This one goes a level down: which work runs as a compute shader, which runs as an ordinary drawing shader, which is still plain code, and how a tool is put together. Where something is only planned, it says so.

Three kinds of work

A graphics card can run two kinds of program. A drawing shader paints shapes and pictures into a picture. A compute shader is a small program that runs over a grid of numbers with nothing drawn at all, thousands of copies side by side. The third kind of work is ordinary code on the processor. The app uses all three, and most of it is the first.

The flood, step by step

Fill needs to know which pixels are like the one you pressed and joined to it. The card answers this in two stages.

The fill then stores its pixels as a list of runs (row, start, length). That read-back is the part not yet on the card.

Enlarging an export

Exporting at 2, 4 or 8 times makes each pixel a sharp square. A compute shader does it in one pass: each pixel of the large picture takes the colour of the small pixel it lies in. Because a card's mistakes are reported late, the app first runs a tiny trial of four colours and checks the result. A card that fails is not used, an ordinary canvas does the job instead, and the Export window says which way was taken.

Planned, not built

A drawing is a list of ops

Every mark is an op: a small record of what was done. A stroke op holds its points as whole numbers (positions in sixteenths of a pixel, pressure from 0 to 255) and its brush settings. A fill op holds the runs it filled. Nothing changes the drawing except applying an op, and opening a saved drawing replays its ops through the same door. So the drawing is its ops in order, and Undo, saving and shared drawings all come from that one fact.

Undo does not keep whole pictures. For each op drawn, the app keeps the small rectangle the op changed, as it was before, cut from a second copy of the layer that always runs one op behind. Undoing the last op puts one patch back and draws nothing. When no patch can answer (an older op, or a change too large to keep a patch of), the app goes back to a whole copy of the layer that it keeps every so often and redraws the ops after it.

Strokes run at two speeds

While the pen is down, a stroke is drawn as stamps, one soft disc after another. That is quick and can be added to as the pen moves. Stamps in a row leave a faintly scalloped edge, though, and on a large brush you can see it.

So when the stroke lands, it is drawn again as pieces. Each pair of neighbouring stamps is joined by the shape a disc sweeps as it slides from one to the next, growing or shrinking to the next size. The sheet is cut into square tiles, and each tile is one drawing job given the pieces that touch it. For every pixel it takes the strongest cover of any piece, not the sum, which is what makes the joins invisible. The soft edge is looked up from a small table worked out once per stroke from what a row of stamps adds up to.

The stamps stay the single source. Pieces are made from the stamp list alone, and stamps are placed from the op's integers, so replaying a stroke gives the same stamps and the same pieces. Only strokes where this is safe get pieces: the round brush with soft edges, even strength, overlapping stamps. Others stay as stamps.

Adding a tool

A tool is a few small pieces, each in one place.

How it is tested

The engine has a suite of tests that run with no screen, so nothing in them may depend on pixels: replays, spacing of stamps, how pieces are cut, the fill against its definition. The page has its own suite. For what only eyes can judge, a headless browser opens a new drawing, draws, and saves a picture to look at, and another run exports images through the real Export dialog.

Links