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>
58 lines
2.4 KiB
JavaScript
58 lines
2.4 KiB
JavaScript
// Ported from party-stage's "Classic Wave". The original picked a hue from a
|
|
// rolling HSV ramp; here the ramp indexes the palette instead, so the look
|
|
// generator can actually recolour it.
|
|
|
|
export const classicWave = {
|
|
name: 'Classic Wave',
|
|
family: 'flow',
|
|
kind: 'fragment',
|
|
|
|
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', 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 },
|
|
},
|
|
|
|
reactive: {
|
|
bloomCore: { feature: 'beat', amount: 0.4, response: 'spike' },
|
|
rings: { feature: 'bandMid', amount: 0.12 },
|
|
},
|
|
|
|
shader: `
|
|
vec4 scene(vec2 uv, vec2 p) {
|
|
float d = length(p);
|
|
float angle = atan(p.y, p.x);
|
|
float t = u_time * u_speed + u_seed;
|
|
|
|
float wave = sin(d * u_rings - t * 2.0);
|
|
float spoke = u_spokes > 0 ? sin(angle * float(u_spokes) + t) * u_beat : 0.0;
|
|
|
|
float v = 0.5 + 0.5 * sin(wave + spoke);
|
|
v = mix(v, smoothstep(0.2, 0.8, v), u_softness);
|
|
|
|
vec3 col = palRamp(t * u_colorRoll + d * 0.25) * v;
|
|
|
|
// Core glow, the part that reads as the "hit".
|
|
col += pal(0) * (1.0 - smoothstep(0.0, 0.7, d)) * u_bloomCore * 0.6;
|
|
|
|
// Keep the corners from clipping to flat colour.
|
|
col *= 0.6 + 0.4 * (1.0 - smoothstep(0.8, 1.8, d));
|
|
|
|
return vec4(col, 1.0);
|
|
}
|
|
`,
|
|
};
|
|
|
|
export default classicWave;
|