Commit Graph

7 Commits

Author SHA1 Message Date
Dejvino
1ca9fa2f1f Let scenes layer on each other, and give the camera somewhere to look
Two devices that were built, wired up, and doing nothing.

OVERLAYS. buildStack gated layering on `surfaceOf(m) === 'composable'`, and
no scene in the library declared `surface` — so the only thing that could ever
sit on top was the one scene declaring `role: 'accent'`, and the overlay roster
excluded accents by construction. Empty intersection, every time: 0/144 stacks
carried an overlay. Every layered frame in every song was the same particle
field.

Labelled the library from the phase 12 coverage gate rather than by eye: the
37 scenes painting under 30% of the frame are composable. Particle Field loses
its privileged slot and becomes one of them, keeping only `background: false`,
which is the honest part — points in empty space cannot carry a section alone.
The reserved accent slot is gone; one roster, two passes at it. 1 distinct
overlay scene becomes 27, and the rate lands at 34% of stacks after trimming a
base chance that had been tuned while the branch was dead.

THE CAMERA. framing.shift moved the frame by a median of 0.029 of a half-frame
at a fresh random angle every shot, so successive offsets cancelled and the
median jump at a cut was 0.014. Present in every frame, visible in none.

look/Camera.js is the director's camera department: the story says tension,
order and which act a section is in, and this turns that into where the frame
looks and how it travels there. Each director names a camera. Jump distance
follows tension and act, speed follows energy, and the curve is one of four.
Cuts choose between reframing and matching, so two scenes can still read as one
place. Median offset 0.190, median reframe 0.135, and 94% of shots now move
during the shot rather than only at the cut.

Four new gates, each with a FLOOR — the device this replaces passed every
existing check while doing nothing, because the only bound was a ceiling.

Also lands the in-progress Epic 4 story layer it builds on: Story.js, phase 13,
and the direction statistic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 06:48:13 +02:00
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
2d3ed8eb02 Finish the migration: all 65 scenes on the identity artifacts
Two helpers made the bulk of it mechanical. `inkStroke` is a drop-in for
sigEdge — the same line at the identity's weight rather than the track's — and
`castForm` is a drop-in for sigForm, same signature so call sites do not change
shape. With those in place the substitution table is one-to-one:

    sigForm(  -> castForm(     sigShape( -> castMain(     sigEdge( -> inkStroke(

Forty-seven scenes went through that pass in one run: twenty-six take the cast
and the ink, twenty-one take the ink alone. Then the gate ran over all sixty-five
and found three the pass had broken, which is the entire reason it exists.

Spectrum Sculpture rendered pure black. It had been using sigShape as a RADIAL
METRIC rather than drawing it, and the cast carries notches and a hollow — an
annulus used as a radius turns a sculpture inside out. Reverted to sigShape and
dropped to ink only. The lesson generalises: a scene that reads a form as
geometry is not a scene that draws it, and the classifier cannot tell those
apart from the source.

Eclipse Field stopped honouring its `style` trait. It opts out of surface grain,
so sigEdge was its only style evidence, and the ink replaced it. The trait claim
is now dropped — and so is the lint change that had let inkMask count as style
evidence, which was wrong and was hiding exactly this. A trait is a property of
the track a scene may honour; an artifact is content it draws. Taking the ink
says nothing about whether a scene responds to u_sigLine.

Circuit Bloom went empty at the bottom of its `grown` range, where the pads were
carried by a hairline and the ink's stroke is thinner than the edge it replaced.
Now filled as well as stroked.

Also: the backtick check now covers every shader literal rather than only the
preamble, because a mechanical pass over sixty files reintroduced one
immediately. Three rounds lost to that typo is enough.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 06:39:19 +02:00
Dejvino
17a583a87f Side quest 1: give four scenes art direction other than grain
Classic Wave, Silk Ribbon, Kaleido Tunnel and Slow Orb all declared the
`style` trait and honoured it with `col += sigGrain(uv)` and nothing else. A
declared trait is a contract — the disqualification rule in Personality.js is
the only thing keeping off-design scenes out of a track — so honouring it
with dirt meant these four could not take the `texture: 0` opt-out the grain
work introduced. Phase 9 measured their style response at exactly 0. They sat
at texture: 0.35 as a stopgap, which kept speckle on the library's cleanest
scenes purely to keep a gate green.

Each already had the knob; it just was not wired to the track:

- Classic Wave contrasts its wave through a bare smoothstep(0.2, 0.8). The
  transition width now comes from u_sigSoft and u_sigLine, centred on 0.5 so
  changing the hand does not change the exposure. Crest concentration is
  driven separately by u_sigLine, because a track is free to sample the
  scene's own u_softness at zero and the art direction must still show.
- Silk Ribbon's strand width is u_sigLine and its falloff exponent u_sigSoft:
  a sharp track gets a filament with a defined edge, a soft one a haze.
- Kaleido Tunnel drew its grid against a bare 0.42 — a line weight with no
  name. It is now a weight and a feather, both from the track.
- Slow Orb's body edge multiplies the scene's softness by the video's, and
  gains a sigEdge rim so a sharp-handed track gets a defined limb.

All four are now texture: 0. Style response measured against a maximally
soft-handed versus maximally sharp-handed personality: Classic Wave 111,
Silk Ribbon 216, Kaleido Tunnel 206, Slow Orb 102, out of 255 — previously 0.

104/104 checks pass including the slow set. See SIDE-QUESTS.md §1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 19:35:50 +02:00
Dejvino
14d1204e82 Make grain a treatment, tempo a governor, and params commit harder
Grain was in every video. It was added twice unconditionally — every scene
called sigGrain, and the grade added its own on top — so the only thing that
varied between two tracks was how much of it there was. That makes grain the
renderer's fingerprint rather than a decision about one video.

It is now described rather than dialled (look/grain.js): a mode (off /
constant / swell / sections / transient), a cell size in pixels, a refresh
rate in frames, a mask (uniform, shadows, highlights, edges, bands) and a
chroma amount. Roughly 45% of tracks get none at all. The non-constant modes
carry a per-frame envelope computed in Show._postAt from frame and features
only, so preview and export still agree. Scene-side grain is gated the same
way, and a module can decline it outright with `texture: 0` — crisp line work
should stay crisp. The post tab grew a real grain block so any of it can be
forced per track.

Slow songs got fast scenes. `motion` bias was mostly section energy with
tempo as a small correction, so a 70bpm track's drop asked for nearly as much
speed as a 150bpm one. Motion is now tempo-dominated, and every `rate: true`
param is additionally scaled by a per-track rateScale — measured, 84bpm now
samples its rate params at 0.276 of range against 148bpm's 0.571.

Parameter sampling also commits harder: extremity starts at 0.45 rather than
0.25 and shapes the draw more aggressively. This was first pushed to 0.82 and
backed off to 0.72, because the gates caught the overshoot — seeds began
collapsing onto the same range ends and a sparse scene sampled at its low end
rendered effectively black.

Fallout worth recording: turning the default grade grain off exposed two
scenes that were never really animating. Dust Chamber and Eclipse Field
passed the Phase 7 movement gate only because per-pixel noise was moving
underneath them; both now breathe on their own fixed clock, and Dust Chamber
needed a brightness floor as well. Four scenes (Classic Wave, Silk Ribbon,
Kaleido Tunnel, Slow Orb) express the style trait ONLY through grain and so
cannot opt out yet; they hold a reduced share at 0.35 pending real edge and
softness response.

Phase 3's look-space check now measures its closest pair relative to image
brightness, the same correction Phase 10 already documents for sparse scenes:
the absolute number was being propped up by grain rather than by look-space
width. Five new Phase 10 checks cover grain distribution, treatment variety,
envelope range, the texture opt-out, and tempo. 93/93 pass.

Two transport bugs, both stale state surfacing in the UI:

- Loading a track replaces audio.src, which stops playback silently, so
  state.playing stayed true and the play button stayed on pause — the first
  click after a track change only flipped the flag back. Added stopPlayback().
- The controls row wrapped mid-song because two readouts that change length
  while playing were sized by their content: the clock crossing ten minutes
  and the section label whenever a scene name is long or a crossfade appears.
  The clock is now fixed-width with tabular figures and the section label is
  the row's only flexible item, laying out at zero width and ellipsising into
  whatever space is left. The two spacers competed with it for that space and
  are gone; its own text-align does their job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 07:04:27 +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