Phase 5: compositing depth ("C" complete)

Multi-layer stacks with blend modes, feedback, post chain, and a 3D
particle layer proving the compositor is genuinely hybrid. Looks now
generate accent layers from a different family, composited additively at
low opacity, weighted by section energy so intros stay sparse.

Adds flash-rate safety (engine/flash.js), which was not in the original
plan and should have been. This generates beat-reactive video for
publication, and rapid light-dark cycling is the photosensitive-epilepsy
trigger; WCAG 2.3.1 caps it at three flashes per second. Classic Wave
measured 7-8/s at every output resolution from 96x54 to 1920x1080, so it
was a real hazard rather than a sampling artefact.

Root cause was general, not one bad shader: `u_time * u_speed` where speed
is reactively modulated. Phase is elapsed*rate, so changing the rate at
time T jumps phase by T*delta — sixty seconds in, a 0.05 wobble throws the
phase three whole units between consecutive frames, and it worsens as the
track runs. Fixed by introducing rate params:

- schema flag `rate: true` documents and marks them
- Layer.resolveParams skips reactivity on them
- ArcDriver skips drift on them
- validateModule rejects a reactive entry on one
- lint-scenes greps shaders for `u_time * u_X` and fails if X is unmarked,
  so no future scene can reintroduce it

Every rate param across the six scenes is now marked. Two checks were
needed to find this: a per-look flash check, and a per-SCENE sweep at
aggressive params, since the look generator only samples part of the space
and a scene can hide an unsafe region for a long time.

Gate 10/10. Worst flash rate now 1/s. Feedback stable over 10,000 frames
(luminance 0.17-0.59, no saturation or decay). 0.17ms/frame at 1280x720.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dejvino
2026-08-05 11:38:26 +02:00
co-authored by Claude Opus 5
parent 5f25437b89
commit 59180948d0
14 changed files with 594 additions and 20 deletions
+9 -3
View File
@@ -10,8 +10,15 @@ export const classicWave = {
params: {
rings: { type: 'float', range: [4, 40], default: 18, uniform: 'u_rings', bias: 'density' },
spokes: { type: 'int', range: [0, 12], default: 5, uniform: 'u_spokes' },
speed: { type: 'float', range: [0.2, 2.5], default: 1.0, uniform: 'u_speed', bias: 'motion' },
colorRoll: { type: 'float', range: [0, 0.5], default: 0.1, uniform: 'u_colorRoll' },
speed: { type: 'float', range: [0.2, 2.5], default: 1.0, uniform: 'u_speed', bias: 'motion', rate: true },
// colorRoll is capped low on purpose. palRamp() steps through all six
// palette entries per unit of t, and the palette generator deliberately
// spreads their luminance — so this multiplies into a whole-frame
// brightness oscillation at six times its own rate. At the original
// range of [0, 0.5] this scene measured 7 flashes per second at every
// output resolution, well past the WCAG 2.3.1 ceiling of 3. Capped here,
// the worst case is ~0.9 Hz. See engine/flash.js.
colorRoll: { type: 'float', range: [0, 0.06], default: 0.02, uniform: 'u_colorRoll', rate: true },
softness: { type: 'float', range: [0, 1], default: 0.4, uniform: 'u_softness' },
bloomCore: { type: 'float', range: [0, 1.5], default: 0.5, uniform: 'u_bloomCore', bias: 'energy' },
palette: { type: 'palette', count: 5 },
@@ -20,7 +27,6 @@ export const classicWave = {
reactive: {
bloomCore: { feature: 'beat', amount: 0.4, response: 'spike' },
rings: { feature: 'bandMid', amount: 0.12 },
speed: { feature: 'loudness', amount: 0.15, response: 'smooth' },
},
shader: `
@@ -10,8 +10,8 @@ export const floatingGeometry = {
params: {
count: { type: 'int', range: [2, 14], default: 6, uniform: 'u_count', bias: 'density' },
size: { type: 'float', range: [0.04, 0.3],default: 0.15,uniform: 'u_size' },
drift: { type: 'float', range: [0.1, 1.5], default: 0.4, uniform: 'u_drift', bias: 'motion' },
spin: { type: 'float', range: [0, 2], default: 0.6, uniform: 'u_spin' },
drift: { type: 'float', range: [0.1, 1.5], default: 0.4, uniform: 'u_drift', bias: 'motion', rate: true },
spin: { type: 'float', range: [0, 2], default: 0.6, uniform: 'u_spin', rate: true },
boxRatio: { type: 'float', range: [0, 1], default: 0.5, uniform: 'u_boxRatio' },
aura: { type: 'float', range: [0, 1], default: 0.25,uniform: 'u_aura', bias: 'energy' },
spread: { type: 'float', range: [0.3, 1.2], default: 0.8, uniform: 'u_spread' },
@@ -21,7 +21,6 @@ export const floatingGeometry = {
reactive: {
size: { feature: 'beat', amount: 0.18, response: 'spike' },
aura: { feature: 'bandHigh', amount: 0.4 },
spin: { feature: 'bandLow', amount: 0.2 },
},
shader: `
+1 -1
View File
@@ -14,7 +14,7 @@ export const nebula = {
swirl: { type: 'float', range: [0, 3], default: 1.0, uniform: 'u_swirl', bias: 'motion' },
rings: { type: 'float', range: [0, 1], default: 0.5, uniform: 'u_rings' },
glow: { type: 'float', range: [0, 1.5], default: 0.5, uniform: 'u_glow', bias: 'energy' },
speed: { type: 'float', range: [0.1, 1.5],default: 0.5, uniform: 'u_speed', bias: 'motion' },
speed: { type: 'float', range: [0.1, 1.5],default: 0.5, uniform: 'u_speed', bias: 'motion', rate: true },
depth: { type: 'float', range: [0, 1], default: 0.4, uniform: 'u_depth' },
palette: { type: 'palette', count: 4 },
},
@@ -16,14 +16,13 @@ export const psychedelicDrift = {
warp: { type: 'float', range: [0, 0.6], default: 0.2, uniform: 'u_warp', bias: 'energy' },
beams: { type: 'int', range: [0, 5], default: 3, uniform: 'u_beams' },
symbolSize: { type: 'float', range: [0.03, 0.14], default: 0.07, uniform: 'u_symbolSize' },
speed: { type: 'float', range: [0.05, 0.8], default: 0.2, uniform: 'u_speed', bias: 'motion' },
speed: { type: 'float', range: [0.05, 0.8], default: 0.2, uniform: 'u_speed', bias: 'motion', rate: true },
palette: { type: 'palette', count: 6 },
},
reactive: {
warp: { feature: 'beat', amount: 0.35, response: 'spike' },
symbolSize: { feature: 'bandLow', amount: 0.25 },
speed: { feature: 'buildSlope', amount: 0.4, response: 'smooth' },
},
shader: `
@@ -12,7 +12,7 @@ export const synthwaveRun = {
kind: 'fragment',
params: {
speed: { type: 'float', range: [0.3, 4], default: 1.5, uniform: 'u_speed', bias: 'motion' },
speed: { type: 'float', range: [0.3, 4], default: 1.5, uniform: 'u_speed', bias: 'motion', rate: true },
gridDensity:{ type: 'float', range: [0.5, 3], default: 1.0, uniform: 'u_gridDensity', bias: 'density' },
horizon: { type: 'float', range: [-0.3, 0.3],default: 0.0, uniform: 'u_horizon' },
sun: { type: 'float', range: [0, 1], default: 1.0, uniform: 'u_sun' },
@@ -24,7 +24,6 @@ export const synthwaveRun = {
reactive: {
glow: { feature: 'beat', amount: 0.45, response: 'spike' },
speed: { feature: 'bandLow', amount: 0.3 },
},
shader: `