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:
@@ -68,7 +68,7 @@ export class ArcDriver {
|
||||
plan = [];
|
||||
for (const [name, def] of Object.entries(spec.module.params || {})) {
|
||||
if (def.type === 'palette' || def.type === 'bool' || def.fixed) continue;
|
||||
if (def.noDrift) continue;
|
||||
if (def.noDrift || def.rate) continue; // see schema.js RATE_FLAG
|
||||
const [lo, hi] = def.range || [0, 1];
|
||||
plan.push({
|
||||
name,
|
||||
|
||||
@@ -144,11 +144,40 @@ export function generateLook(track, { seed = null, samples = null, overrides = n
|
||||
const sceneByKind = assignScenesByKind(track.sections, rng.fork('scenes'));
|
||||
const { post, feedback } = derivePost(summary, rng.fork('post'));
|
||||
|
||||
// Scenes eligible as accents: 3D layers composite over a shader background
|
||||
// without fighting it, so they are preferred where available.
|
||||
const accentRoster = scenes.filter((m) => m.kind === 'layer3d');
|
||||
|
||||
const sections = track.sections.map((section) => {
|
||||
const module = sceneByKind.get(section.kind) || scenes[0];
|
||||
const sectionRng = rng.fork(`section:${section.index}:${module.name}`);
|
||||
const bias = biasFor(section, summary);
|
||||
|
||||
const layers = [{
|
||||
module,
|
||||
params: sampleValues(module, sectionRng, bias),
|
||||
seed: sectionRng.int(0, 0x7fffffff),
|
||||
blend: 'normal',
|
||||
opacity: 1,
|
||||
}];
|
||||
|
||||
// Accent layer. Composited additively over the background at low opacity,
|
||||
// and drawn from a DIFFERENT family so it reads as depth rather than as a
|
||||
// second competing scene. Quiet sections mostly go without — an intro is
|
||||
// supposed to be sparse.
|
||||
const accentChance = bias.energy * 0.8;
|
||||
if (accentRoster.length && sectionRng.bool(accentChance)) {
|
||||
const accent = sectionRng.pick(accentRoster.filter((m) => m.family !== module.family) || accentRoster)
|
||||
|| accentRoster[0];
|
||||
layers.push({
|
||||
module: accent,
|
||||
params: sampleValues(accent, sectionRng.fork(`accent:${section.index}`), bias),
|
||||
seed: sectionRng.int(0, 0x7fffffff),
|
||||
blend: sectionRng.pickWeighted(['add', 'screen'], [2, 1]),
|
||||
opacity: sectionRng.range(0.18, 0.5),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
index: section.index,
|
||||
kind: section.kind,
|
||||
@@ -158,13 +187,7 @@ export function generateLook(track, { seed = null, samples = null, overrides = n
|
||||
end: section.end,
|
||||
locked: false,
|
||||
bias,
|
||||
layers: [{
|
||||
module,
|
||||
params: sampleValues(module, sectionRng, bias),
|
||||
seed: sectionRng.int(0, 0x7fffffff),
|
||||
blend: 'normal',
|
||||
opacity: 1,
|
||||
}],
|
||||
layers,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user