# No Paint Construct operation migration ledger The surviving export has no `.c3p` source or source map. Its authorities are `nopaint.art/data.json`, the expression table in `nopaint.art/scripts/c3runtime.js`, the sprite sheets, and `nopaint.art/media/`. ## Reading the export No numeric constant is stored in `data.json`. An event parameter is `[type, [expressionNumber, ...nodes]]`, and the value lives in `C3_ExpressionFuncs[expressionNumber]` — a compiled function inside `c3runtime.js`. Two tools make that readable, and a recovered constant is not recovered until one of them has been run: ```sh node toolchain/nopaint/decode-sheet.mjs Caterpillar # a sheet, expressions resolved node toolchain/nopaint/expressions.mjs 376 # one expression by number node toolchain/nopaint/expressions.mjs --grep hsla # search the table ``` Reading raw index numbers as if they were literals is how earlier passes produced constants that were never in the original. Two are corrected below. The canonical picker vocabulary contains 38 names: `Aura, Banner, Blur, Breathe, Bubbles, Build, Box, Camera, Caterpillar, Contrast, Dark Window, Ellipse, Flip, Grid Worm, Invert, Light Bump, Line, Load, Mirror, Noise, Quicksand, Rainbow, Recurse, Saturate, Scroll, Sharpen, Softy, Spin, Stamp, Turn, Triangle, Vignette, Wafer, Walker, Wipe, Zoom, Frame, Playlist`. ## Brush migration The non-conflicting brush names migrated as deterministic proposal contracts are `aura`, `banner`, `breathe`, `bubbles`, `build`, `caterpillar`, `dark-window`, `ellipse`, `frame`, `grid-worm`, `rainbow`, `softy`, `triangle`, `vignette`, `wafer`, and `walker`. Exact disk-name conflicts are `blur`, `box`, `camera`, `line`, `noise`, and `wipe`. They are deliberately not recreated under those names. The existing AC `line` contract remains usable by No Paint, but is not claimed as a migration of the Construct implementation. `stamp` was on that list until the existing `disks/stamp.mjs` grew a `nopaintProposal` beside its own brush, the way `box` already had. ## Piece names versus operation names `disks/` is one flat folder and a file name is a URL, so every recovered brush spends a word out of the whole site's namespace. Usually that is fine and the piece takes the operation's name. Twice it is not: - `box.mjs` owns the operation `rect`. - `frames.mjs` owns the operation `frame` — `frame` is too useful a word to spend on one brush, and `Frames` is what Construct called the object. The operation name is what `nopaintProposal.slug` and the recovered catalogs use; the piece name is what `brush.slug` records, so a score always names the piece you would rerun. Nothing resolves `brush.slug` to a disk automatically — it is provenance — but keep it pointing at a real piece. ## Pieces that own their slug Every recovered brush now has one: `bubbles`, `walker`, `dark-window`, `line`, `box`, `stamp`, `frames`, `caterpillar`, `softy`, `wafer`, `grid-worm`, `triangle`, `ellipse`, `rainbow`, `breathe`, `vignette`, `aura`, `build`, and `banner` are real AC pieces that export their own `nopaintProposal`; `disks/nopaint.mjs` imports each one and registers it after the fallback catalog, so the piece's contract wins by slug. `nopaint-construct-catalog.mjs` is down from fourteen fallbacks to two, and the two left — `bubbles` and `walker` — are already owned by their pieces through import order. It is an inventory now, not an implementation. ## Three of them were never brushes `rainbow` and `breathe` draw nothing. Their sheets put a Construct effect over the whole painting — AdjustHSL and Bulge — and walk its parameter on a timer, so both belong with the pixel transforms and rewrite the accepted painting instead of proposing into the buffer. `vignette` and `aura` lay a field rather than a shape. Reading a name as "a brush" because it sits in the brush picker is the third kind of mistake this migration kept making, after misreading expression indices as literals and missing function blocks. ## Corrections Two constants in the first pass came from misreading expression indices as values. Both are now read out of `C3_ExpressionFuncs`: - **Frame** cycles every **1** second, not 5, starting at index 1 and advancing `(frameIndex + 1) % AnimationFrameCount`. The knock plays at a `random(.25, 2)` playback rate each cycle. - **Caterpillar** has no `1 / 3 / 32` segment choice and no `.2 / .7` scale range. `length = ProcessNumericParameter(1, 3, 32) - 1` is parameter slot 1 over the range 3–32, and `.2`–`.7` is `saturation = random(.2, .7)`. Asking for seven segments (`Caterpillar: 7`) is the original rainbow-road easter egg: `length == 6` swaps the cue and cycles hue along the body instead of fading lightness. ## Redrawing versus accumulating Most recovered brushes are a function of the frame, so `render` can redraw them whole. Softy is not: it stamps a soft circle into a layer on every Move tick, and the stroke is the accumulation. Redrawing the whole stroke each frame costs **~1.7s a frame** at the largest radius, so Softy keeps a per-score layer, composites only the stamps the clock has newly reached, and pastes that layer once — peak 7ms on the first frame, 0.4ms after. Any future brush that paints a trail rather than a shape wants the same treatment. Because a Construct stroke ran until you pressed Paint and a proposal has to end, Softy's walk is capped at a fixed number of stamps. That length is the one invented number in it. Wafer accumulates the same way, for a different reason: a bite *erases* (Construct switches to blend mode 7, cuts an ellipse, switches back), so the biscuit is only ever the sum of what has been taken out of it. ## Reading a brush's real work Most of a brush's behaviour lives in **function blocks**, not in the sheet's top-level events — the sheet just calls `Bite`, `BiscuitEnlarge`, `CircularBite`. A function block is a type-`4` node carrying its name in `node[1][0]` with the same conditions/actions/children tail as an event, and `decode-sheet.mjs` prints them as `function `. If a decoded sheet looks too thin to be the whole brush, the work is in the functions. The safe image filters/transforms `contrast`, `flip`, `invert`, `light-bump`, `mirror`, `quicksand`, `recurse`, `saturate`, `scroll`, `sharpen`, `spin`, `turn`, and `zoom` are also migrated and wired into the proposal conductor. `load` and `playlist` are control/meta operations and are deliberately not presented as visual brushes. ## Fidelity boundary Recovered score constants, deterministic state, timing values, grid/frame choices, and available primary sound samples are retained. Construct-only effects including Exclusion blending, Bulge, AdjustHSL, Vignette and sprite timeline behaviors have no equivalent in the current AC ink API. Their AC renderers use bounded translucent primitives and do not claim pixel parity. Dark Window selects and plays each of its four original note samples, and Caterpillar picks between `trotting along` and `rain bow road` from its own score. Build and Wafer have multi-cue event vocabularies — Wafer alone has five, one per bite plus appear and enlarge — while the current No Paint conductor owns one active brush sample at a time. Their primary sample is wired, and the full vocabulary is recorded in each contract's `source`; exact per-event cue sequencing requires a conductor audio-timeline API. Stamp's `mirrored` is real: Construct mirrored on a negative X scale, and `paste` now reaches that by handing `grid` a per-axis `{x, y}` scale. `grid` had the flip scaffolding already but measured each reversed cell backwards, so every negative-scale draw came out empty; `spritePaste` flips the frame origin to `1 - ox` to keep a mirrored sprite in the same box. Two recovered behaviours are still carried in the score but not drawn, because the AC surface has no equivalent yet: Caterpillar's per-segment tint (Construct's Set color — `paste` has no colour multiply) and Frame's per-cycle knock playback rate.