Comfort: agitation ceiling, composition validator and whole-screen caps

Whole-screen flashing/coherent motion is now a gated comfort signal
rather than a hunt-and-tweak. Three-arm agitation (flash/3,
tile-motion/0.32, structure/0.12) over a 1s floating window, worst
window wins — engine/agitation.js.

Policy centralised in look/comfort.js: director comfort appetite,
per-scene agitationOf proxy, agitationWeight lean and stackAgitation.
Canvas beat/flux capped in Layer.resolveParams (0.15/0.25, spike→smooth)
so one contract change tames every full-frame scene; lint warns above
the cap.

Selection now filters before casting (look/scene-filter.js, 12+ ranged
metrics from checks/scene-metrics.js, SCHEMA 7) and validates after
stacking (look/composition-validator.js) with a deterministic retune:
drop overlay → swap quiet composable → reselect filtered shot. Fix
validator attempt-3 ground-layer bug that crashed ?song=dusk
(LookGenerator now passes module, not layer).

Eight canvas shaders window their beat/flux (focusField + per-seed hash
mask) and report the scaled amount to Layer so the cap stays authoritative:
scan-tear, gate-corridor, analog-wow, block-mosh, metaballs,
pitch-shatter, scale-mosaic, tide-rings.

Checks: scene-gate comfort gate (agitation ≤ 0.70), filmstrip/gallery
meters + per-scene drawer, metadata SCHEMA 7 with ranged metrics and
fingerprint over agitation/comfort sources. main.js surfaces
COMPOSITION_REFUSED as a user-facing error.

Lint: 114 files clean, 71 scenes ok. All 17 songs generate looks.
Build passes. metadata.json refresh deferred to gallery → Refresh
metadata (SCHEMA 7 writer in vite.config).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Dejvino
2026-09-13 19:42:50 +02:00
co-authored by Claude
parent a9b028b0bf
commit 7736390f9c
25 changed files with 1319 additions and 49 deletions
@@ -31,7 +31,7 @@ export const gateCorridor = {
},
reactive: {
lamps: { feature: 'beat', amount: 0.35, response: 'spike' },
lamps: { feature: 'beat', amount: 0.14, response: 'smooth' },
aperture: { feature: 'bandLow', amount: 0.15, response: 'smooth' },
},
@@ -75,10 +75,14 @@ vec4 scene(vec2 uv, vec2 p) {
col += tint * frame * (0.25 + near * 0.75);
col += tint * exp(-d * 14.0) * near * 0.2;
// Beat window — only every third gate's lamp carries the beat,
// and it is masked by focal proximity so the corridor does not flash as one.
float gateBeatMask = step(0.68, hash12(vec2(fi * 0.41, floor(u_seed * 0.3)))) * focusField(p) * 0.9 + 0.1;
float maskedLamps = u_lamps + gateBeatMask * u_beat * 0.45;
// A lamp at the top of every third gate, pulsing on the beat.
if (mod(fi, 3.0) < 0.5) {
vec2 lampAt = vanishAt + vec2(0.0, scale);
col += pal(4) * exp(-length(p - lampAt) * 26.0) * u_lamps * (0.3 + near);
col += pal(4) * exp(-length(p - lampAt) * 26.0) * maskedLamps * (0.3 + near) * (0.6 + 0.4 * gateBeatMask);
}
}