From 14d1204e820a4a68f686e0d3a7106261883c23e6 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Thu, 6 Aug 2026 07:04:27 +0200 Subject: [PATCH] Make grain a treatment, tempo a governor, and params commit harder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grain was in every video. It was added twice unconditionally — every scene called sigGrain, and the grade added its own on top — so the only thing that varied between two tracks was how much of it there was. That makes grain the renderer's fingerprint rather than a decision about one video. It is now described rather than dialled (look/grain.js): a mode (off / constant / swell / sections / transient), a cell size in pixels, a refresh rate in frames, a mask (uniform, shadows, highlights, edges, bands) and a chroma amount. Roughly 45% of tracks get none at all. The non-constant modes carry a per-frame envelope computed in Show._postAt from frame and features only, so preview and export still agree. Scene-side grain is gated the same way, and a module can decline it outright with `texture: 0` — crisp line work should stay crisp. The post tab grew a real grain block so any of it can be forced per track. Slow songs got fast scenes. `motion` bias was mostly section energy with tempo as a small correction, so a 70bpm track's drop asked for nearly as much speed as a 150bpm one. Motion is now tempo-dominated, and every `rate: true` param is additionally scaled by a per-track rateScale — measured, 84bpm now samples its rate params at 0.276 of range against 148bpm's 0.571. Parameter sampling also commits harder: extremity starts at 0.45 rather than 0.25 and shapes the draw more aggressively. This was first pushed to 0.82 and backed off to 0.72, because the gates caught the overshoot — seeds began collapsing onto the same range ends and a sparse scene sampled at its low end rendered effectively black. Fallout worth recording: turning the default grade grain off exposed two scenes that were never really animating. Dust Chamber and Eclipse Field passed the Phase 7 movement gate only because per-pixel noise was moving underneath them; both now breathe on their own fixed clock, and Dust Chamber needed a brightness floor as well. Four scenes (Classic Wave, Silk Ribbon, Kaleido Tunnel, Slow Orb) express the style trait ONLY through grain and so cannot opt out yet; they hold a reduced share at 0.35 pending real edge and softness response. Phase 3's look-space check now measures its closest pair relative to image brightness, the same correction Phase 10 already documents for sparse scenes: the absolute number was being propped up by grain rather than by look-space width. Five new Phase 10 checks cover grain distribution, treatment variety, envelope range, the texture opt-out, and tempo. 93/93 pass. Two transport bugs, both stale state surfacing in the UI: - Loading a track replaces audio.src, which stops playback silently, so state.playing stayed true and the play button stayed on pause — the first click after a track change only flipped the flag back. Added stopPlayback(). - The controls row wrapped mid-song because two readouts that change length while playing were sized by their content: the clock crossing ten minutes and the section label whenever a scene name is long or a crossfade appears. The clock is now fixed-width with tabular figures and the section label is the row's only flexible item, laying out at zero width and ellipsising into whatever space is left. The two spacers competed with it for that space and are gone; its own text-align does their job. Co-Authored-By: Claude Opus 5 --- flow-state/.claude/launch.json | 11 + flow-state/HOWTO-visualizers.md | 9 + flow-state/PLAN.md | 29 +++ flow-state/index.html | 6 +- flow-state/src/Show.js | 29 ++- flow-state/src/checks/phase10.js | 155 +++++++++++++- flow-state/src/checks/phase3.js | 18 +- flow-state/src/engine/Compositor.js | 16 +- flow-state/src/engine/Layer.js | 2 +- flow-state/src/engine/passes.js | 37 +++- flow-state/src/look/LookGenerator.js | 37 +++- flow-state/src/look/Personality.js | 26 ++- flow-state/src/look/grain.js | 193 ++++++++++++++++++ flow-state/src/main.js | 118 ++++++++++- flow-state/src/params/schema.js | 26 ++- flow-state/src/scenes/shader/cargo-belt.js | 2 + flow-state/src/scenes/shader/circuit-bloom.js | 2 + flow-state/src/scenes/shader/classic-wave.js | 3 + flow-state/src/scenes/shader/dust-chamber.js | 20 +- flow-state/src/scenes/shader/eclipse-field.js | 10 +- .../src/scenes/shader/floating-geometry.js | 2 + flow-state/src/scenes/shader/gate-corridor.js | 2 + .../src/scenes/shader/girder-lattice.js | 2 + flow-state/src/scenes/shader/horizon-lines.js | 2 + .../src/scenes/shader/kaleido-tunnel.js | 3 + flow-state/src/scenes/shader/moire-grid.js | 2 + flow-state/src/scenes/shader/prism-bloom.js | 2 + flow-state/src/scenes/shader/pylon-grid.js | 2 + flow-state/src/scenes/shader/quasicrystal.js | 2 + flow-state/src/scenes/shader/silk-ribbon.js | 3 + flow-state/src/scenes/shader/slow-orb.js | 3 + .../src/scenes/shader/spectrum-sculpture.js | 2 + flow-state/src/scenes/shader/truchet-fold.js | 2 + flow-state/src/ui/style.css | 22 +- 34 files changed, 756 insertions(+), 44 deletions(-) create mode 100644 flow-state/.claude/launch.json create mode 100644 flow-state/src/look/grain.js diff --git a/flow-state/.claude/launch.json b/flow-state/.claude/launch.json new file mode 100644 index 0000000..6b35578 --- /dev/null +++ b/flow-state/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "flow-state", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 5180 + } + ] +} diff --git a/flow-state/HOWTO-visualizers.md b/flow-state/HOWTO-visualizers.md index 93556d1..2e83bc2 100644 --- a/flow-state/HOWTO-visualizers.md +++ b/flow-state/HOWTO-visualizers.md @@ -112,6 +112,15 @@ intentionally shrinks per track. Only declare what you genuinely use: The lint greps your shader and fails a declared trait with no evidence. Declaring `[]` (none) is valid. +### `texture` — how much surface grain your scene accepts + +Optional module field, `0..2`, default `1`. It scales `u_sigTexture` — and so +every `sigGrain(uv)` in your shader — for this scene only. Set it to `0` if your +scene is crisp line work that grain only furs up, or to something under 1 if it +should be dusted rather than dirty. It has nothing to do with the *grade's* +grain, which is a per-track treatment (see `look/grain.js`) and is off entirely +for most tracks. + ## Traps that have actually bitten here 1. **Anything multiplying `u_time` must be `rate: true`.** Phase is diff --git a/flow-state/PLAN.md b/flow-state/PLAN.md index ee44581..0d8e53d 100644 --- a/flow-state/PLAN.md +++ b/flow-state/PLAN.md @@ -579,6 +579,35 @@ Adding a scene was mostly boilerplate and round-trips, which is expensive in bot a verdict, instead of rendering the whole library to find out whether one shader is alive. - `.claude/skills/build-visualizer/` is the same procedure as a repo skill. +### Phase 10b — grain as a treatment, tempo as a governor + +Watching the library again surfaced two things no gate was asking about. + +**Grain was in every video.** It was added twice unconditionally — every scene called +`sigGrain`, and the grade added its own on top — so the only thing that varied between two +tracks was how much. That makes grain the renderer's fingerprint rather than a decision about +one video. It is now DESCRIBED (`look/grain.js`): a mode (`off` / `constant` / `swell` / +`sections` / `transient`), a cell size in pixels, a refresh rate in frames, a mask (uniform, +shadows, highlights, edges, bands) and a chroma amount. Roughly 45% of tracks get none at all; +the non-constant modes have a per-frame envelope computed in `Show._postAt` from frame and +features only, so preview and export still agree. Scene-side grain is gated the same way, and +a module can decline it outright with `texture: 0` — crisp line work should stay crisp. + +**Slow songs got fast scenes.** `motion` bias was mostly section energy with tempo as a small +correction, so a 70bpm track's drop asked for nearly as much speed as a 150bpm one. Motion is +now tempo-dominated, and every `rate: true` param is additionally scaled by a per-track +`rateScale`, so absolute animation speed follows the song rather than only the sampled +position in a range. + +Parameter sampling also commits harder: `extremity` starts at 0.5 rather than 0.25 and shapes +the draw more aggressively, because a range is the scene author's statement of what the scene +can survive and a library that samples the middle of every range shows every scene's default. + +**Gate:** a quarter to two thirds of tracks have no grain and at least four modes appear; +thirty grainy tracks produce mostly-distinct treatments; every time-varying grain reaches both +zero and full; a `texture: 0` scene gets nothing even from a maximally gritty track; and a +84bpm track's rate params sample materially below a 148bpm track's. + --- ## 10. Detachment from `party-stage` diff --git a/flow-state/index.html b/flow-state/index.html index d54ac03..51ee63c 100644 --- a/flow-state/index.html +++ b/flow-state/index.html @@ -32,9 +32,11 @@ 0:00 / 0:00 - + -