The contract
You draw on the canvas. brush(point, stroke, t) runs for every point your hand makes.
point - x, y, pressure (real on a pencil), speed (px/s), angle (direction your hand is heading), index.
Apple Pencil: the Pencil pressure dial (in Hand) lets pressure grow every mark's size, and add more marks per touch on brushes that return a list - press soft for fine lines, hard for bold ones. 0 turns it off; finger and mouse are unaffected. You can also read point.pressure yourself inside brush() for full control.
stroke - length, age (seconds), seed (a random number that stays the same all stroke).
Return one mark or a list: { x, y, size, alpha, red, green, blue, shape, soft, glow, angle, life, vx, vy }. life loops: the mark fades out, then is reborn where it started (0 = stays forever).
Optional move(mark, t) gives every mark a life - same words as Paint dots' force: dx, dy, x, y, size, alpha, red, green, blue. mark has ox, oy (where it was born), age, seed, index, and its birth angle in degrees.
Optional function background(t) - the paper color. glow marks shine brightest on a dark background.
Optional function warp(x, y, t, field) - return { dx, dy, alpha, blur, shape } to move the loaded photo itself through a 2D vector grid. shape: "circle" clips each displaced source cell into a round sample instead of the default square. field.centerX / centerY is the center of that disconnected painted mask island; field.edgeDistance is the inward distance from its edge and field.maxEdgeDistance says whether the island has a real interior, useful for feathering without erasing a thin stroke. alpha is 0-1 and blur is 0-40; omitted values mean opaque and crisp. Needs a photo + the Photo background slider above 0.
Helpers you can always call: noise(x, y, z) - smooth wind-like randomness; dist(x1, y1, x2, y2); rand(seed); photo(x, y, blur) - the uploaded photo's color there, sampled with blur 0-40 (or null); rebuildPhoto(mark, opacity, blur) - recolor a mark (or a list) from the photo where each lands; TAU.
Stage settings are code too: FLOW (marks per touch, 1-12), SIZE (2-80), FADE (0-100), SPEED (1-100), LINKS (0-200, connect nearby marks), TERRITORY (1 = every mark owns its space), PHOTO_OPACITY (0-100) and PHOTO_BLUR (0-40). These constants apply to the next stroke/layer: opacity multiplies the photo-colored mark's alpha; blur samples a softer photo and raises that mark's soft. Already-painted strokes keep their own settings.
REBUILD_PHOTO - the top-level choice in every brush, off by default (it flips itself on the moment you load a photo): 1 = the brush ends with finishBrush(marks), which calls rebuildPhoto(marks, PHOTO_OPACITY, PHOTO_BLUR) so marks take the photo's colour. 0 = draw with the brush's own colours (edit them with the colour picker above). No photo loaded = nothing changes either way.
When you upload a photo, it becomes the visible paper/source image and cancels preset background(t). PHOTO_OPACITY never fades that source image; it only controls the opacity of new photo-colored marks. PHOTO_BLUR is visible in the helper code through photo(m.x, m.y, blur) and m.soft.
And rebuildPhoto is not hidden anywhere: every preset ends with the actual function rebuildPhoto(marks, opacity, blur) in the editor - read it, edit it, break it, make it yours.
While your hand is down, the painting holds still and new marks appear plainly - the whole painting (moves, fades, ripples) comes alive again the moment you lift. This keeps drawing smooth on an iPad.
In Function brush mode, code is the brush. Click Apply brush, or just start drawing - if the editor changed, the page tests and applies it before the new stroke begins. Older strokes keep the brush code they were born with.
Use the Brush / Eraser buttons to rub marks away, and paint from a photo you upload - it all stays on this device. Without a photo, the coded background(t) remains the paper behind the marks.