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:
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.mjsowns the operationrect.frames.mjsowns the operationframe—frameis too useful a word to spend on one brush, andFramesis 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 arandom(.25, 2)playback rate each cycle. - Caterpillar has no
1 / 3 / 32segment choice and no.2 / .7scale range.length = ProcessNumericParameter(1, 3, 32) - 1is parameter slot 1 over the range 3–32, and.2–.7issaturation = random(.2, .7). Asking for seven segments (Caterpillar: 7) is the original rainbow-road easter egg:length == 6swaps 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 <name>. 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.