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:
@@ -120,6 +120,26 @@ console.log('\nscene schema lint');
|
||||
fail(`${id}: shader reads ${uniform}, which no param declares (it will silently be 0)`);
|
||||
}
|
||||
|
||||
// Rate params: anything the shader multiplies absolute time by must be
|
||||
// flagged `rate: true`, which stops reactivity and drift from touching it.
|
||||
// Modulating such a param jumps the phase by elapsed*delta — sixty seconds
|
||||
// in, a wobble of 0.05 throws the phase by three units between frames.
|
||||
// That measured as 6 flashes/second on Classic Wave, twice the WCAG 2.3.1
|
||||
// ceiling, and it gets worse the longer the track runs.
|
||||
const timeProducts = [
|
||||
...src.matchAll(/u_time\s*\*\s*(u_[A-Za-z0-9_]+)/g),
|
||||
...src.matchAll(/(u_[A-Za-z0-9_]+)\s*\*\s*u_time/g),
|
||||
];
|
||||
for (const match of timeProducts) {
|
||||
const uniform = match[1];
|
||||
const paramName = declared.get(uniform);
|
||||
if (!paramName) continue;
|
||||
if (!module.params[paramName].rate) {
|
||||
fail(`${id}: '${paramName}' (${uniform}) multiplies u_time but is not marked ` +
|
||||
`\`rate: true\` — reactivity or drift on it will cause phase jumps`);
|
||||
}
|
||||
}
|
||||
|
||||
// A scene with a palette param should actually use the palette helpers,
|
||||
// otherwise the look generator cannot recolour it.
|
||||
const hasPalette = Object.values(module.params || {}).some((d) => d.type === 'palette');
|
||||
|
||||
Reference in New Issue
Block a user