--- name: build-visualizer description: Add a new visualizer (scene) to flow-state, or fix one that is failing its gates. Use when asked to build/add/write a visualizer, scene, or shader for this project, when a scene fails lint or the phase gates, or when the scene library needs another entry in some family. Covers the scaffolder, the shader contract, the personality traits, and the single-scene gate loop. --- # Build a visualizer A scene is **a fragment shader plus a params block**. Uniform binding, UI sliders, per-track sampling, arc drift and every gate are derived from the schema — there is no per-scene wiring to write. The full reference is [HOWTO-visualizers.md](../../../HOWTO-visualizers.md). Read it once for the contract details. This file is the *procedure*, and following it in order is what keeps a new scene from costing several rounds of trial and error. ## The loop ```bash npm run new:scene -- "Salt Flat" --family=minimal --traits=shape,camera,space,style ``` That writes `src/scenes/shader/salt-flat.js`, registers it, and leaves a skeleton that already passes every gate — live, animated, seeded, distinct from every other scene, honouring exactly the traits it declares. Add `--feedback` if the scene will read `prev()`. Then, in order: 1. **Write the concept comment first.** Two lines: what it looks like, and what makes it different from the scenes it sits beside. "No two scenes render the same image" is a gate with a numeric floor, not a guideline. If you cannot write the second line, the scene does not exist yet. 2. **Replace the `scene()` body.** Keep the skeleton's trait calls; they are what the casting rule is checked against. 3. **Lint.** `npm run lint:scenes` — one second, no browser, catches schema and shader disagreeing, undeclared uniforms, missing `rate: true`, a declared trait with no evidence, a dead camera, `prev()` with no base image, and unbounded loops. 4. **Gate the one scene.** Open `http://localhost:5180/checks.html?scene=Salt%20Flat`. Ten-ish lines: schema, renders, animates, deterministic, distinct, param sweep, flash rate, and one line per declared trait. This is the same battery Phases 2, 5 and 7 apply library-wide, filtered to your scene. 5. **Run the library gates** once at the end: `checks.html?slow=1`. Phases 2, 5 and 7 iterate the registry, so the new scene is covered automatically. Do not skip 3 before 4, or 4 before 5. Each step is roughly ten times cheaper than the next and catches a different class of mistake. ## Choosing family and traits **Family** decides which section kinds can cast the scene — a breakdown never lands on a strobing glitch scene. Aim for 4-6 scenes per family; check the current spread with: ```bash node -e "import('./src/scenes/registry.js').then(({scenes})=>{const b={};for(const m of scenes)(b[m.family]??=[]).push(m.name);console.log(b)})" ``` **Traits** are a contract, not a hint. Each track is built on a signature of one or two traits and **a scene that does not honour all of them is never cast in that track**. Declare only what the shader genuinely uses: | trait | call | what it means for your scene | |---|---|---| | `shape` | `sigShape(p)` / `sigForm(p, at, size)` | every element you draw is the track's signature form, not your own circle or box | | `camera` | `sigCamera(p)` | your coordinate is filmed by the track's operator. **Must feed the image** — assigning it to a `p` you then ignore is a dead camera, and both the lint and the render gate will say so | | `space` | `sigHorizonY()`, `sigAir(col, p, d)` | your ground is at the track's horizon and your distance haze is the track's | | `style` | `sigEdge(d)`, `sigGrain(uv)`, `sigFolded(p)`, `u_sigLine/Soft/Texture/Fold` | your lines are drawn in the track's weight | Prefer thin traits. `space` and `shape` carry the most identity and have the fewest scenes, so they are usually where another scene is worth most. ## The five mistakes that actually happen 1. **A rate param that is not flagged.** Anything multiplying `u_time` needs `rate: true`, or reactivity jumps the phase by `elapsed × Δrate` and the scene strobes. Add a bounded term instead: `u_time * u_speed + u_bandLow * 2.0`. 2. **Whole-frame luminance on the beat.** That is the WCAG 2.3.1 failure the flash gate exists for. Pulse something local; quantise glitches onto `floor(u_barPhase * n)` so they step with the music. 3. **A declared trait the image does not respond to.** Passes the eye, fails the gate. Both cost the same to fix before you commit and much more after. 4. **Reading `prev()` with nothing underneath.** Black for the first frames, different after a seek than after playback. Always draw a base field. 5. **A contract uniform name reused as a param** (`u_width`, `u_time`, `u_seed`). GLSL redefinition; the only symptom is a black frame. ## When a gate fails - **not distinct** — the closest scene is named in the output. Change the structure, not the palette; colour comes from the track. - **dead/blown in the param sweep** — the sweep pushes each param to its limits alone. Usually a range that should not reach 0, or one that saturates. - **flash rate over 3/s** — find the term that swings the whole frame and make it local or smooth. - **not deterministic** — something is reading wall-clock or unseeded randomness; the lint greps for the usual suspects, but `fwidth`-style derivative tricks can also differ. Everything must be a function of `u_time`/`u_frame` and `u_seed`.