Commit Graph

5 Commits

Author SHA1 Message Date
Dejvino
e2e5104646 Take the effects off the visualizers and leave them to the grade
Forty-nine scenes carried their own `glow`, every one of the sixty-five added
its own grain, and a handful had bloom, chroma, haze, smear or trail besides —
all of which the post chain already does, with an envelope and a threshold no
scene can see. Two uncoordinated effect layers is the smaller half of the
problem. The larger half is that an effect knob sampled per song makes a scene
look varied across parameter draws while its structure never moves, so the
gallery and the variety harness were both being told a scene was original when
only its halo had changed.

Removed in three shapes. Scene grain goes entirely — the post chain owns it.
Statements that ADD a falloff term scaled by the effect are bloom by another
name and are deleted. What remains is an intensity on something already drawn,
so the uniform is pinned to its default and the knob deleted: the picture
survives, the fake variety does not. Reactive entries driving those params went
too, since an effect modulated by the audio was the most convincing fake of the
lot.

That broke the style trait, which is the interesting part. Surface treatment had
been leaving the scenes for two commits — sigGrain to post, sigEdge to
inkStroke — and removing the last of it left `style` with almost nothing to
express: a style+shape signature had two eligible scenes in the whole library.
The trait is not obsolete, it has moved, so inkStroke and inkMask now read
u_sigLine and u_sigSoft alongside the ink's own weight and edge. A scene drawing
in the song's hand honours the track's line weight by construction, and the
runtime probe confirms it rather than taking it on trust. This reverses a call
made two commits ago for a reason that only became true now.

One check needed re-aiming rather than fixing. Two independently built shows are
two WebGL contexts, and engine/hash.js says at the top that bit-exactness is a
same-context guarantee; the check had been demanding it anyway and getting away
with it because the scenes it happened to cast were bit-stable. A casting change
put smooth-gradient scenes in frame and the last bit moved. Measured before
touching the check: three levels out of 255 across 2.6% of pixels, invisible.
The cross-context comparison is now a distance with a ceiling of four, and
bit-exactness within one show is still demanded by the check below it.

89/89 checks, 11/11 tests, all static gates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:21:34 +02:00
Dejvino
e5bb7d77e0 A reproducible migration, and the first eighteen scenes through it
The library is sixty-one scenes, which is too many to convert from memory or
from taste, so the migration is a queue with a gate rather than a judgement call
per file.

Three pieces. A classifier reads each shader and assigns a tier from evidence in
the source — drawn, figure, field, treatment — so two passes over the library
reach the same answer and the work has an order. MIGRATION.md is the recipe per
tier, written to be followed mechanically. And a gate makes the result
verifiable: `consumes` is now a schema field, the lint enforces it in both
directions, and the per-scene battery renders each scene under two deliberately
distant identities and requires the picture to change.

That gate is the part that matters. Without it `consumes` is a comment, and the
whole inversion becomes unverifiable at exactly the point where it stops being
checkable by eye. With it, a scene that declares the cast and ignores it fails.

Eighteen scenes migrated. Four by hand at the drawn tier — Firefly Drift,
Metaballs, Floating Geometry, Prism Bloom — and ten at the field tier by script,
which is one declaration and one wrapped return. All eighteen pass.

The field tier is honestly marginal and the gate says so: every one of the ten
moves by 37 to 39 of 255, against 173 to 255 for the drawn tier, and the
uniformity across ten unrelated scenes is the tell. That is one global
posterisation applying, not ten scenes expressing anything. Cheap, real, shallow.

The decomposition moved from identity being worth 54% of the container to 158%,
but the stage set changed underneath the measurement and part of that is
Metaballs expressing a cast better than Constellation did. What survives the
caveat is the useful finding: a migrated library scene carries the identity
better than a stage written from scratch to carry it. The four bespoke stages
were the wrong shape of effort.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:59:24 +02:00
Dejvino
189328587d Epic 2.4: a slow axis, and an honest account of what it fixes
Eleven scenes changed as much in half a second as in two minutes. The cause
was that everything moving in them was cyclic: ArcDriver's drift is a 20-70
second LFO, and an LFO returns, so ten cycles of it over five minutes is not
five minutes of anything. The eye adapts in about two seconds and then there
is nothing left to find — violently animated, and reads as static.

ArcDriver._slowAxisFor adds the missing timescale: a param that travels ONE
WAY across the whole track, keyed on the module so a scene returning in the
last section arrives further along rather than resetting. Rate params are
excluded for the reason schema.js already gives.

Two things were learned by getting them wrong first, and both are recorded
where the next person will hit them.

The first version chose the param at random from everything eligible and
measured as doing NOTHING — identical structural change with the axis applied
and with it disabled. Which param you move decides everything: sweeping Moiré
Grid's `width` moves its time-averaged structure by 0.110 and its `offset` by
0.002, and a random draw finds the second kind almost every time. So the axis
is now DECLARED, `slowAxis: true`, validated by the schema (and rejected on
rate params, where walking one would jump the animation phase).

The second is that single-frame distance cannot measure this at all. A
churning scene's consecutive frames are already ~0.6 apart, so every pair of
its frames scores the same whether the structure moved or not — the metric is
saturated by the churn it exists to see through, and the first gate passed
while the mechanism was provably inert. The gate now compares TEN-SECOND
time-averaged frames. The window was measured rather than guessed: at one
second Moiré Grid's frozen control still reads 0.024, at ten it reads 0.0095
while the signal holds at 0.037.

Per EPIC-2.md §4, the metric is verified by breaking what it should catch:
a second check runs the identical measurement with the axis disabled and
requires it to read ~1.0. It reads exactly 1.00x on all three scenes.

The honest scope, measured across ten candidates and written into EPIC-2.md
§3.4: the axis works on Moiré Grid (3.93x), Gate Corridor (2.88x) and Truchet
Fold (1.40x). Curl Flow, Signal Decay and Circuit Bloom have NO parameter that
changes their structure and still need shader work — parameter automation
cannot substitute for structure a scene does not have. Four more already
develop on their own and were never the problem. The flag is set only where it
is proven, so it means something.

69/70 on phases 3,4,7-11 slow; the one failure is the known load-dependent
Horizon Lines flake recorded in d14d473.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 07:54:35 +02:00
Dejvino
ca5a68eb84 Phases 8 and 9: shots, and a production design per track
Both phases come out of the manual gate — watching whole tracks — and both
fix something no automated check was looking for.

Phase 8: shots. A section is a STAGE of the song and can run ninety
seconds; one scene held that long reads as a still image with a wobble on
it. Each section kind now gets a roster of three or four stage visuals
instead of one scene, and each section is cut into shots that rotate
between them on phrase lines, never holding past 22s. The roster stays per
kind, so a track's drops still cut between the same images and the video
keeps its identity; the anchor opens each section and the rotation returns
to it, and when a companion is due it is the least recently shown one.

The arc driver stopped working in sections and started working in cues, one
per shot, so a shot cut and a section change take the same code path and
differ only in transition length. The default transition is a slow
dissolve — two bars calm, one loud; a straight cut is reserved for
sections above the energy threshold, because on calm material a cut reads
as a glitch rather than as an edit.

Phase 9: production design. With cuts every fifteen seconds the next
problem was that the images being cut between shared nothing but the
palette. What a music video actually shares across shots is a location, a
cast, a camera operator and an art direction, so each track now generates a
personality in four traits (shape, camera, space, style) off the look seed.
The traits reach shaders as uniforms plus four helpers in the contract, and
each scene expresses them its own way: Classic Wave's rings take the
signature polygon, Metaballs merge as one, Floating Geometry no longer
picks between a box and a circle because the production already decided.

The part that makes it a design rather than a filter: scenes DECLARE which
traits they honour, a track is built on one or two, and a scene that does
not honour all of them is not cast in that track. The library shrinks per
track on purpose.

Two gates keep the declaration honest — lint greps each shader for evidence
of every trait it claims, and a render check measures that each declared
trait actually moves the image (41 scene/trait pairs, weakest response 64
of 255). A layer with no personality renders bit-identically to before,
which is what keeps every earlier sweep and regression valid.

Checks changed rather than added:
  - P4 scene-change and drift checks now measure per shot, not per section;
    the crossfade check reads its length off the cue.
  - P5 flash sweep runs per shot, so the visuals that only appear
    mid-section are measured too.
  - P6 preview/export parity primes first (as both real paths do) and
    compares at the one-LSB tolerance Phase 7 already uses. Measured over
    four consecutive shows: 3 frames at delta 1, then bit-exact — GPU
    variance on first render, not a divergence.
  - P2's contract-uniform list is derived from the contract instead of
    retyped, so the signature uniforms cannot fall out of sync.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:04:33 +02:00
Dejvino
9d15c3cf49 Phase 7: grow the scene library to 16
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>
2026-08-05 18:40:56 +02:00