Actors and objects change only at camera cuts. int/bool/discrete params skip drift, lookahead and reactivity; discrete slowAxis steps on the section-held journey. Venues fade instead of toggling on interpolated popMul; roadside density is arrangement-only. DESIGN.md collects the project's design decisions; AGENTS.md points agents at it and the build-visualizer loop.
155 lines
8.4 KiB
Markdown
155 lines
8.4 KiB
Markdown
# flow-state — design decisions
|
||
|
||
The decisions this project has taken, and why. New work must obey them;
|
||
changing one means updating this file and the gate that enforces it.
|
||
|
||
Doc map: `PLAN.md` is the full design, `EPIC-2/3/4/5-3D.md` are the epic
|
||
designs, `HOWTO-visualizers.md` / `HOWTO-variety.md` are the scene-author
|
||
references, `MIGRATION.md` is the shader→stage migration recipe.
|
||
|
||
## 1. Determinism spine
|
||
|
||
- The audio is fully analysed **before the first frame renders**, into a
|
||
frame-indexed `FeatureTrack` table. Nothing reads a live `AnalyserNode`.
|
||
Preview maps `currentTime → frameIndex`; export counts frames. Same rows,
|
||
same image.
|
||
- No wall-clock (`performance.now`, `Date`) and no `Math.random` in
|
||
`engine/scenes/look/audio/params/actors/venues`. Seeded `Rng` only;
|
||
motion is analytic `f(t, seed)` — seek === playback, preview === export.
|
||
- `dt` is constant (`1/fps`); dropped preview frames hitch, never diverge.
|
||
- Feedback/ping-pong targets are explicitly cleared; shader programs are
|
||
primed (`Engine.prime`) before frame-exact rendering — unlinked programs
|
||
rendered the first ~10 frames wrong on the heaviest scene.
|
||
- Scene math lives in normalized aspect-corrected coords (`uv`, `p`); only
|
||
`u_pixelScale` carries pixels, so 720p preview matches 4K export.
|
||
- Guarantee is **per-machine** bit-identical; cross-machine is perceptual.
|
||
Same-machine allows max channel delta ≤ 1 (GPU load varies one LSB under
|
||
heavy shaders); real bugs score in the tens.
|
||
- Seeks need **warm-up** (`log(0.001)/log(decay)` frames) whenever feedback
|
||
is on. Section boundaries are NOT exact for free — the compositor's
|
||
feedback buffer is global and carries across them. Clearing it per
|
||
boundary would flash every transition; warm-up is the better trade.
|
||
|
||
## 2. Lookahead
|
||
|
||
Offline analysis is what buys anticipation: `buildSlope` ramps through the
|
||
bars before a higher-energy section, so visuals arrive at the drop already
|
||
at tension. A causal analyser can never do this. The outgoing layer of a
|
||
crossfade keeps its pre-boundary slope — feeding it the new section's
|
||
features collapses its ramp in one frame (a visible pop at the edit).
|
||
|
||
## 3. Scenes: shader + schema, nothing else
|
||
|
||
- A scene is a fragment shader (or model build/update) plus a params block.
|
||
Uniform binding, UI sliders, seeded sampling, arc drift and phase gates
|
||
are all derived from the schema. No per-scene wiring.
|
||
- Six families (`flow geometric organic structural minimal glitch`) chosen
|
||
by section kind — a breakdown never lands on a strobing glitch scene.
|
||
- Personality traits (`shape camera space style`) are a **contract**: each
|
||
track is built on 1–2 traits and a scene honouring fewer is never cast.
|
||
The library intentionally shrinks per track. Declared traits need
|
||
machine-checked evidence (lint grep + render-delta gate); a dead camera
|
||
(`sigCamera` assigned, result ignored) shipped once and is now linted.
|
||
- `rate: true` is mandatory for anything multiplying `u_time`. Phase is
|
||
`elapsed × rate`; modulating a rate jumps phase by `elapsed × Δrate` —
|
||
measured as 6 flashes/s, twice the WCAG 2.3.1 ceiling.
|
||
- Whole-frame luminance never pulses on the beat (3-flashes/s ceiling).
|
||
Pulse something local; quantise glitches to the bar grid.
|
||
- A scene reading `prev()` must paint a base image first (black first
|
||
frames, seek-fragile otherwise). `prev()` readers can never be grounds.
|
||
- No contract-uniform collisions, no hardcoded saturated colours — the
|
||
look generator recolours through `pal()`/`palRamp()` only.
|
||
- Loops need constant bounds with an early `break` on the deciding param
|
||
(`// lint: fixed-cost` opt-out for genuinely fixed work).
|
||
|
||
## 4. Continuity: nothing disappears mid-shot
|
||
|
||
Actors and objects change only at camera cuts:
|
||
|
||
- **Discrete params** (`int`/`bool`, plus floats marked `discrete: true`
|
||
that drive loop bounds / instance counts) hold constant for the whole
|
||
cue. No drift, no lookahead ramp, no reactive modulation — those would
|
||
pop members in/out mid-shot. They still vary per section/variant sample,
|
||
so drops can be denser than intros; the change lands on a cut.
|
||
- A discrete param MAY declare `slowAxis`: the driver steps its travel on
|
||
the section-held journey instead of sliding it.
|
||
- Reactive on a discrete param and (unstated) drift of one are lint errors.
|
||
- Venue furniture never hard-toggles on interpolated state: chamber junk
|
||
and stage pylons fade (ride-crew pattern); ride roadside density is
|
||
arrangement-only. Re-hangs happen only on story jumps, always under a
|
||
camera cut (`venueJumpAt`); the venue layer itself is persistent and
|
||
never dissolved (old-room/new-room ghosting read as a glitch).
|
||
|
||
## 5. Time structure: cues, cuts, camera
|
||
|
||
- The arc driver works in **cues** (one per shot): a shot cut and a section
|
||
change take the same code path, differing only in transition length.
|
||
- Default transition is a slow dissolve (2 bars calm, 1 bar loud, capped
|
||
at 40% of the shot). Hard cuts only above the energy threshold — on calm
|
||
material a cut reads as a glitch. Even hard cuts are 2-frame ramps (a
|
||
1-frame swap of bright scenes is a flash).
|
||
- Framing lives at the shot level (size constant per shot, gaze live);
|
||
the outgoing shot keeps travelling as it fades — freezing it stops the
|
||
old image dead before it disappears.
|
||
- Sharp transitions cut colour instantly with the image and always reframe
|
||
(a bigger reframe than a dissolve earns).
|
||
- Story variables are **staged, not ramped**: flat inside a section,
|
||
blended ~1 bar after boundaries. Moments (arrival/turn/climax/resolution)
|
||
are found in the audio; the story never overrides the song (bounded
|
||
bias terms). All scenes share one axis sign so the video travels one way.
|
||
|
||
## 6. Colour and variety
|
||
|
||
- Palettes in OKLCH; hue from **spectral tilt** (log treble/body ratio —
|
||
centroid and plain body-fraction both collapsed across masters); both
|
||
ways round the wheel; directors choose 1–4 palettes, blended in OKLCH
|
||
across dissolves.
|
||
- Per-track **temperament** (intensity/pace/detail/extremity) moves every
|
||
param dial so one scene cast in two videos isn't the same footage twice.
|
||
- **Overlays** on 5–55% of stacks, never `normal` blend, never above 0.6
|
||
opacity — occasionally layered, never permanently cluttered.
|
||
- A scene varies when the SONG changes what's on screen: draw
|
||
`castMain`/`castChorus` (`form` in 3D), size via `stageScale()`, place
|
||
via `stageNode()`. No effect-knobs (glow/grain/chroma belong to post).
|
||
- Grain is a per-track **treatment** (mode/cell/rate/mask/chroma, ~45% of
|
||
tracks get none); scene `texture: 0..2` scales only its share.
|
||
- Absolute animation speed follows the song: motion bias is
|
||
tempo-dominated and every `rate: true` param scales by per-track
|
||
`rateScale` — slow songs got fast scenes before this.
|
||
|
||
## 7. Grounds and composition
|
||
|
||
- `surface` is measured (`metadata.json`), never declared — nine scenes
|
||
claimed `canvas` while painting under a third of the frame.
|
||
- A ground fills the frame on average and never vanishes under any
|
||
identity (worst case counts, not the mean); hollow grounds are
|
||
flattened via `groundPersonalityFrom`.
|
||
- Model layers share one `Compositor` perspective/depth rig; shader depth
|
||
stays faked. Venue rooms are furnished with the track's monolith body
|
||
so the room reads as this track's place.
|
||
|
||
## 8. 3D on the same terms
|
||
|
||
- `ActorSpec` is data, not scene graph (serialisable, hashable); geometry
|
||
via `actorToGeometry` from the same `Identity.form` the shaders march.
|
||
- Model motion is analytic, instanced, seeded; kit deformation reuses the
|
||
song's silhouette params; `castSolid` impostors remain the fallback.
|
||
- New actors are a file plus a registry entry, gated like scenes.
|
||
|
||
## 9. Export and review
|
||
|
||
- WebCodecs primary, frame-dump+ffmpeg fallback; title card, edge fades,
|
||
progress/cancel; A/V sync within one frame; survives a real upload test.
|
||
- Preview is the primary surface: transport, section jump/loop, live param
|
||
edit, timeline strip, re-roll + locks, draft/full quality, 20s
|
||
segment test-renders. Never commit to a full render blind.
|
||
- Every phase gate is measured; the battery (6 tracks ambient→hard EDM)
|
||
is watched end to end — monotony is invisible to automation.
|
||
|
||
## 10. History
|
||
|
||
- Forked from `party-stage`, then detached: zero cross-imports.
|
||
- Corrections kept: boundary seeks need warm-up (not exact for free);
|
||
`layout` is blind on full-frame fields; aggregate ratios (e.g.
|
||
`separation`) swing run-to-run — steer by direct render comparisons.
|