Ten new scenes, with 'minimal' first: the family was empty, so intros and
breakdowns fell through to flow/organic and every track opened at full
density. Quiet sections now land on a restful family 48/48 times across 24
seeds, 28 of them minimal.
New: Horizon Lines, Spectrum Sculpture, Slow Orb (minimal); Curl Flow
(flow); Plasma Bloom, Metaballs (organic); Kaleido Tunnel, Moiré Grid
(geometric); Ridge Terrain (structural); Scan Tear (glitch).
Four real bugs, three of which the existing gates could not have caught:
1. SHADER PROGRAMS LINK ASYNCHRONOUSLY. three.js uses
KHR_parallel_shader_compile, so draws against an unlinked program render
wrong. The heaviest scene had its first TEN frames differ from every
later render of the same frames. Preview hides this entirely; export
renders each frame once, so those frames would ship broken. Added
Engine.prime() — WebGLRenderer.compile() plus a discarded warm frame —
and the exporter now primes before encoding. Rendering a throwaway frame
and reading it back is NOT sufficient; measured, it left 3-5 frames wrong.
2. Moiré Grid declared a param on u_width, which the shader contract already
uses for stereo width. GLSL redefinition, and the only symptom was a
black frame. Lint now rejects any param uniform colliding with the
contract.
3. Spectrum Sculpture strobed at 4 flashes/s. Two causes: rotation measured
in turns meant bar-crossing frequency was bars x rate (82 bars put a
slow-looking 0.12 turns/s at 10 Hz), and hard band-tier boundaries made
every bar switch band simultaneously. Rotation is now in segment units so
the rate IS the crossing frequency, bands interpolate, and the range is
capped where the flash meter measures zero.
4. Particle Field was being chosen as a primary background despite being
mostly empty by design. Scenes now declare role: 'accent'; those are
never primary and are judged on variance rather than luminance.
Three checks were themselves wrong and were rebuilt: mean-distance metrics
unfairly fail sparse scenes for being tasteful rather than static, so
"animates" and "no duplicates" now use max channel delta.
PLAN.md §1 gains two refinements: programs must be primed before the first
frame, and even same-machine the heaviest shaders vary by one LSB under
differing GPU load — so the per-scene criterion is max delta <= 1 rather
than an identical hash. A real bug scores in the tens there.
Full suite 67/67 across all seven phases. Worst 4K frame 3.8ms,
worst flash rate 0/s, worst determinism delta 1/255. Adds README.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Engine core: Timeline (fixed dt, audio-mastered in realtime), seeded Rng,
Renderer, Layer/ShaderLayer/SceneLayer, Compositor with blend modes,
feedback and post chain.
Shader scenes are compiled against a fixed uniform contract and define only
`vec4 scene(vec2 uv, vec2 p)`, so adding a scene costs a shader plus a
params block. Deep Nebula ported from party-stage as the first one.
Gate passes, 7/7 in checks.html:
- 300 frames rendered twice are bit-identical
- a fresh Engine reproduces the same frames
- simulated dropped frames change nothing (proves dt is fixed)
- seek matches sequential playback
- 320x180 vs 1280x720 agree within 0.010 (limit 0.06)
- seeded rng reproducible, forked streams independent
- compositor reset clears feedback history
Static gates: no wall-clock or unseeded randomness in deterministic
directories; scene schemas and shader sources agree in both directions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>