One album must not render the same dungeon twice. dungeonTheme() scores the measured summary per theme — pace terms gate on tempoConfidence so doubled detectors never read fast, crypt is penalised for blazing masters — plus small seeded jitter so ambiguous songs vary across seeds while decided races stay decided. The plan carries the theme name; dressing mix, planting/timber/puddle densities follow it, and the facade builds all materials (stone/floor multipliers, torch + light color, wood, fog tint) from the same record. Calibrated bank spread is 4/4/4/4/1 (windswept holds runner); corners stable across seeds; plan reshaping, facade wiring, and murk-vs-clear gates covered by tests.
10 KiB
10 KiB
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
FeatureTracktable. Nothing reads a liveAnalyserNode. Preview mapscurrentTime → frameIndex; export counts frames. Same rows, same image. - No wall-clock (
performance.now,Date) and noMath.randominengine/scenes/look/audio/params/actors/venues. SeededRngonly; motion is analyticf(t, seed)— seek === playback, preview === export. dtis 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); onlyu_pixelScalecarries 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. - Previews link to their exact app frame (
src/ui/preview.js: song + seed- frame + venue/viz pins + the probe's warm-up length, both sides primed).
Exactness takes four agreements: the bank probes analyse the bank PCM
(peak-normalised 16-bit mono —
audio/songbank.js bankPcm), file decode is fixed at 44.1 kHz with bit-exact WAV parsing (never the context rate), pins run before any layer builds (rebuilding mid-context renders differently), and materials built mid-update compile synchronously (needsCompile) so no warm-up frame draws against an unlinked program.
- frame + venue/viz pins + the probe's warm-up length, both sides primed).
Exactness takes four agreements: the bank probes analyse the bank PCM
(peak-normalised 16-bit mono —
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 (sigCameraassigned, result ignored) shipped once and is now linted. rate: trueis mandatory for anything multiplyingu_time. Phase iselapsed × rate; modulating a rate jumps phase byelapsed × Δ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
breakon the deciding param (// lint: fixed-costopt-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 markeddiscrete: truethat 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
normalblend, never above 0.6 opacity — occasionally layered, never permanently cluttered. - A scene varies when the SONG changes what's on screen: draw
castMain/castChorus(formin 3D), size viastageScale(), place viastageNode(). 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..2scales only its share. - Absolute animation speed follows the song: motion bias is
tempo-dominated and every
rate: trueparam scales by per-trackrateScale— slow songs got fast scenes before this. - Venues have a per-track character the same way: the dungeon derives
sealed/murky/big (
dungeonCharacter: tonal darkness, doubling-compensated tempo, stillness, weight) from the measured summary, calibrated against the bank because arrangement intent doesn't survive measurement (ambient doubles tempo, hiss reads bright). Dark slow songs seal fully indoor; blazing masters always crack open. - The dungeon also gets a per-track visual theme (
dungeonTheme: crypt/overgrown/ember/flooded/windswept), scored off the same summary traits — one album must not render the same dungeon twice. Pace terms gate on tempoConfidence (doubled detectors must not read fast), jitter is small enough that decided races stay decided across seeds.
7. Grounds and composition
surfaceis measured (metadata.json), never declared — nine scenes claimedcanvaswhile 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
Compositorperspective/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. - With a room present the viz plays ON its screens (window/wall/radio),
never composited over it; model viz additionally stand in the shared
world for shared-rig rooms. A
directCameraroom (ride) owns only its own rig — the shared rig keeps the subject grammar, or the viz is filmed through a room camera meant for another world and vanishes.
8. 3D on the same terms
ActorSpecis data, not scene graph (serialisable, hashable); geometry viaactorToGeometryfrom the sameIdentity.formthe shaders march.- Model motion is analytic, instanced, seeded; kit deformation reuses the
song's silhouette params;
castSolidimpostors 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);
layoutis blind on full-frame fields; aggregate ratios (e.g.separation) swing run-to-run — steer by direct render comparisons.