f8e8d91c9b9647c0558cebc0cb4f5bb9751da861
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f8e8d91c9b |
Epic 5 Phase 2.4 — Pylon Field 3D + Synthwave Corridor (model stages)
Two Stage-A model stages behind the same infra as Assembly:
Pylon Field 3D (structural, kind:model, actor:structure) — chorus
stacked legs + protagonist crowns on the pylon-grid perspective
grid, transparent compositing via ModelLayer, analytic f(t,seed).
Synthwave Corridor (structural, kind:model, actor:vehicle) — grid
verges with streaming chorus passers + monolith hero on the road,
verge math preserved from synthwave-run horizon/roadHalf calc.
Registered in scenes/registry.js; consumes:['form','ink','staging'],
traits:['shape','space','camera','style'] so trait/consumes gates
stay testable (reads personality in update()). Grid texture falls
back to DataTexture headless so lint/build stay green outside a
browser.
Gates: lint 110 clean / 70 literals / 71 scenes; vite build 301 modules.
Co-Authored-By: Claude <noreply@anthropic.com>
|
||
|
|
b98aa94dda |
Stage 3.6 — models visible: transparent model compositing + Assembly density
Model layers render with transparent clear so Compositor lumakey/normal can show composition underneath where no mesh covers — Assembly is now sparse (hero + satellites on ground, not opaque fill), satellites wired to count/spread/density so the density slider visibly changes the stage and a seek is still just f(t,seed). Shared camera plumbing already drove framing->dolly/personality->drift for real parallax. Gates: lint 108 clean / 70 literals / 69 scenes; vite built. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
93a5dc8437 |
Epic 5 Phase 2.3 — instrument Assembly for measurement
Gallery, metadata, scene-gate and scene-sweep now handle kind:'model'
with a deterministic ActorSpec. Drawing uses a forked ActorGenerator
so the actor stream never shifts the identity/palette streams:
gallery.renderScene → generateActor per context for model stages
metadata.measureLibrary now includes model stages, SCHEMA 5,
fingerprint covers actors/
scene-gate.runSceneGate → alive/animates/deterministic/distinct/
param-sweep/flash-rate all drive Engine.setLayerSpecs with
actorSpec, and consumes: searches re-derive the actor for the
swapped identity (so the consumes: gate is still testable).
scene-sweep behaves the same per-cell.
Engine.setLayerSpecs now forwards actorSpec/framing into createLayer
so the checks don't need ArcDriver.
SCHEMA bump 4→5 forces metadata refresh on next gallery build,
which is intentional: model stages now participate. Gates remain
all green (69 scenes) and vite builds clean — no runtime change to
the render path for fragment stages.
Co-Authored-By: Claude <noreply@anthropic.com>
|
||
|
|
16bb67e703 |
Epic 5 Phase 2.2 — actors visible in look description
describeLook now includes describeActorSet so HUD / check output shows which actor archetypes and forms were cast. Look -> actor plumbing is end-to-end: LookGenerator.actors on the look, ArcDriver._actorFor + ModelLayer setActor, Show.renderFrame shared camera injection, all deterministic. Gates: lint 108 clean / 70 literals / 69 scenes green; vite built. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
fcbde602de |
Epic 5 Phase 2.1 — Assembly stage (model)
First kind:'model' stage: Assembly. The song's solid as a mesh. Hero is the ActorSpec(monolith) assembly via actorToGeometry, built from Identity.form with paletteMap recolour — same character as the shader impostor, now with real occlusion/parallax/foreshortening. Analytic motion only (spin/bob/orbit as f(t,seed,params)), ground plane + standard-material lighting that matches castLit key/fill, fog for depth haze, shared camera rig (framing→dolly, personality.camera→ drift/sway/spin). Palette arc visible live via per-frame material recolour; opacity crossfade via material.opacity. Registered in structural family so castingPool / director blends see it like any structural shot. Gates: lint 108 clean / 70 literals / 69 scenes green; vite 294 modules built. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
f60a29ea97 |
Epic 5 Phase 1 — form is a mesh, actors have parity with SDF
meshes.js now mirrors shader-contract.js verbatim: - jsCastSDF rotates by -tilt (matching GLSL column-major mat2), hollow excluded during outer-radius search so annular SDFs sample correctly, radial search probes without hollow then punches hole separately. - castShape samples 64 segments, inner hole scaled by hollow width. - formToGeometry uses formDepth (u_formDepth = r.z*depth half-extent), capsule h correctly half-height, sphere as scaled unit sphere, prism extruded with proper halfDepth and centered; bevel thickness from part.round. - actorToGeometry preserves per-part op/blend userData, uses YXZ Euler for yaw/pitch (matches formRot), mirrors radial/mirror/stack symmetry exactly as shader formFold does. Shared camera wired live: Show.renderFrame drives Compositor.updateSharedCamera from ArcDriver framing/personality/time and injects sharedCamera into each active ModelLayer — pure f(frame,look) so seek === playback. ArcDriver exposes framingForFrame helper. Gates: lint 107 clean / 70 literals / 68 scenes green; vite build 294 modules; deterministic actor geometry smoke-tested (same seed same vert count, hollow handling, fallback). Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
7aa60d7336 |
Epic 5 Phase 0 — actors have bodies; the stage has depth
ActorGenerator is the cast with bodies: generateActor{mpl} takes
{summary,rng,archetype,personality,identity} and returns a serialisable
ActorSpec — same audio-tilts-centre / seed-picks-within rule as
Personality/Identity, forked rng so adding an actor never shifts later
decisions. Five archetypes (monolith/swarm/walker/vehicle/structure),
per-track actor set on look.actors, HUD helper included. Stage C will
grow as a library on this without infra changes.
Mesh twin of Identity.form: actors/meshes.js builds BufferGeometry from
the same assembly (cast SDF → Shape → ExtrudeGeometry, box/capsule/
torus/sphere primitives, symmetry folding radial/mirror/stack). Shared
with the shader impostor path — one character, two projectors.
Renderer depth targets: createDepthTarget / createTarget{depthTexture}
for WebGL DepthTexture plumbing.
Compositor shared rig: one PerspectiveCamera + DepthTexture so a ground
mesh can occlude a subject mesh from another layer. 4/scale dolly,
Personality.camera drift/sway/spin, framing shift — matches particles.js
and shader epilogue behaviour. ModelLayer (kind:model) with
build/update(actorSpec) and sharedCamera injection; createLayer dispatches
on model. Shader contract gains MODEL_PREAMBLE.
LookGenerator now derives actors before scenes; ArcDriver._actorFor +
_layerFor wires ActorSpec into ModelLayer; schema validates kind:model
and actor archetype; lint determinism gate covers actors/.
Gate: lint 107 files clean, 70 shader literals, 68 scenes green; vite
build 294 modules; ActorGenerator determinism + mesh smoke tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
|
||
|
|
ef1d24e692 |
Director cuts between palettes; the look panel shows which one is live
One palette for a whole song reads as one colour once five minutes have passed. The track now keeps 2-4 audio-tilted palettes (forced scheme/hue diversity so switches are legible) and the director owns how they are traversed per cue — sequential/pong/kindLed/storyLed/ contrast — with cut or OKLCH blend timed to the cue crossfade. The fine OKLCH paletteArc drift still rides on top of whichever base is active. The look tab renders the full set as a list; the row matching ArcDriver.paletteIndexAt(frame) gets an active border/glow and follows playback/scrub live, with a blend badge while two palettes interpolate. Co-Authored-By: internal-model |
||
|
|
df609424d5 |
Synthwave verges stay off the road; camera stays level unless the tail says otherwise
Synthwave Run: roadside passers stream on the verges, not on the road. Each passer places at roadHalf + psize*0.92 + gap where roadHalf mirrors the grid's abs(p.x*perspective) < 0.8, so even a large chorus form's edge stays clear of the centre lane reserved for the protagonist. Lateral wobble reduced to worst ~0.025 inward vs. minimum 0.07 gap so it can never carry back onto the road. Four random chorus objects rushing horizon->camera make each song's traffic its own; on-road traffic the hero weaves around is left as a separate kind later. Also carries the previous chorus passer loop, hero castSolid, and texture 0.4 / form+staging declarations still pending in the working tree from the last step. Personality camera: uniform boxes put as many tracks at the wild edge as near level, so the grid swung too often. Replace with a bell curve (Box-Muller over the seeded Rng) — mode at level, rarity in the tails. driftRate/sway/swayRate/spin/breathe now via gaussAround(centre,sigma) centred near level (spin at 0, sign from the Gaussian itself) with audio tilting the centre (fast→more drift/sway/spin, dynamic→breathe) rather than displacing the whole box. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
c216e31413 |
Synthwave Run drives the song's protagonist instead of the same car
The grid, mountains and sun keep their palette-driven forms and are still individually switchable so the arc driver can strip the scene back to just the grid for a breakdown, but the car was the same silhouette in every video — a hexagonal track and a round track drove the same stepped rect with red discs. The thing on the road is now the song's own protagonist: the same solid every other stage in the video draws, seen here as one hero above the grid, turning slowly so its outline changes across the song rather than being one silhouette from every angle. One castSolid instance sized by stageScale() so a song of few huge forms drives a huge hero and a song of many small ones drives a small one, lit by castLit with an inkStroke rim and a soft ground ellipse for contact. Gated by the existing u_car param so a breakdown still drops to just the grid the same way it used to. Declares form, ink, staging and shape/space/camera/style (texture 0.4); the lint's backtick and consumes gates stay green. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
3a0cf26e6c |
The filmstrip fills in as it renders
A full build is a couple of minutes of GPU work, and the page spent all of it showing an empty screen and then everything at once. The interesting failure — a strip whose frames could be shuffled without anyone noticing — is visible in the first row, so waiting for the seventeenth to look at the first is a needlessly slow way to find it out. Rows are appended as they land, in bank order, and re-sorted once at the end when there is finally something to sort by. One row renderer handles both raw pixels, which is what exists mid-build, and the compressed blobs the cache holds afterwards, so what you watch appear is what you keep. The song is announced BEFORE its render rather than only after it, too: synthesising and rendering one is tens of seconds, and a page that says nothing until the first one lands looks broken for exactly as long as that takes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
89e05459c0 |
Every shot stands on something, and the frame has two ends
A section used to be one scene, and two thirds of the library is composable —
sparse by design, elements ON something. Cast as backgrounds anyway, they left
9 of 40 sampled frames under 20% painted, the darkest at 0.3%: a minute and a
half of a few bright things on black, invisible to every gate because every
gate on the stack was a limit rather than a floor.
Every section now stands on a GROUND: a canvas that fills the frame, cast per
section kind so a shot cut changes the shot and not the world. When the shot
fills the frame itself it IS the ground — two canvases stacked is two pictures
fighting. Above that, a coverage BUDGET: director appetite times the section's
energy times where the story is, capped at two frames' worth of material.
The measured facts move into the repo. scenes/metadata.json is generated from
the gallery — coverage as a shot, coverage as a bed, variety, the structural
profile — tracked in git, stamped with a fingerprint of the scenes and the
metric definitions, and refreshed from gallery.html. `surface` is derived from
it rather than declared; nine scenes claimed `canvas` while painting under a
third of the frame, and declaring it is now a lint error. The generator weights
every layering choice by measured structural distance, because family labels
and the render disagree: two `geometric` scenes can be 0.31 apart and a `flow`
and an `organic` scene 0.04.
The gallery's 0.1 red line is gone. It was right when a section was one scene
and wrong now — nineteen scenes were failing a bar for being consistent, which
is a virtue in an ingredient.
Chasing the numbers turned up four real faults:
* A scene that reads prev() cannot be a ground. It returns the whole
composited frame including the layers above it, so a datamosh under a shot
is eating it: the render stopped reproducing from a seek and two WebGL
contexts diverged by 91/255 against a tolerance of 4.
* Screen was the wrong operator for a shot over a bed. It lightens, so a
median quarter of every frame clipped to paper and whole sections rendered
100% white. Replaced by a lumakey — the shot's brightness is its alpha.
* Feedback was an accumulator: a still image settled at 2.3x its own
brightness. Fine over black, fatal over a filled ground. Normalised at 0.6,
plus a highlight shoulder so the top rolls off instead of clipping.
* useTrack never prewarmed, so a fresh Show's first frame differed from every
later render of it — the export-breaking hazard Compositor.prime documents.
Blazing is a decision now, not a side effect: directors declare an appetite for
it, a section must be loud and late in the story to earn one, and quiet kinds
never do. The ceiling gate matches that — a hard cap per section, and no more
than a fifth of them hot at all.
Rendered across twelve videos, middle of every section:
painted 51% mean, darkest 0.3% -> 87% mean, darkest 43%
clipped white 24% median, worst 100% -> 1% mean, worst 30%
separation 0.10 -> 0.44
Seven scenes can ground a section — five geometric, two organic — so every
quiet section of every video stands on one of two beds. That is the library's
largest hole and it is scene work: there is no minimal or flow canvas that
fills half the frame without reading prev().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7d151be6e1 |
Pylon Grid stands on the chorus instead of on a strut
The legs were the last thing in this scene it had invented for itself: a fixed-width vertical capsule, the same object in every video, with only the palette separating one track's structure from another's. Against migrated crowns they read as scaffolding left up around a finished building — and structure is half this frame, so half of it was still saying what the SCENE is rather than what the song is. A leg is now a stack of the song's chorus. How many members make up a column is the song's element size deciding it — a track of few huge forms stands on three, a track of many small ones on a ladder of nine — and how wide the column is comes from the chorus's own proportions. Each member turns at its own angle, so a column is one form seen many ways rather than one form printed nine times. That makes `staging` a third declared artifact, which the lint caught before the gate had to. Measured on the crop that contains the change, since the frame-level score cannot see it: over six identities the bottom third of the frame moves 13.4/255 with the old strut and 22.2/255 with the chorus stacked. The whole-frame variety score goes 0.1322 to 0.1348, and the header now explains why the two disagree and why both are right — the crowns dominate a descriptor averaged over the whole image. Cost is unchanged in practice: 7.7ms/frame at 4K, 12.4ms with every param at the top of its range, against a 60ms ceiling. The column's bounding box is two comparisons for a pixel outside it, which with a hundred and sixty pylons is most of the budget. The crown is resolved before the leg now. Resolved the other way round, the first-wins rule let the column paint over the object standing on it and every subject was cut off at the waist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
61e0c66f61 |
Pylon Grid stands the song's object on its pylons
The scene already had the depth axis the solid wants — near rows large, far rows small, converging on the track's horizon — which made the flat crown the one element in it drawn without perspective: a sticker facing the viewer on top of a receding structure. Each crown is now marched, and each pylon turns its own at its own angle, so a single field shows the form from a dozen sides at once. 12/12 on the gate, 7ms/frame at 4K against a 60ms ceiling. A hundred and sixty instances stay affordable on the two guards MIGRATION.md now documents, and here first-wins is the correct occlusion as well as the cheap one, because the rows are already walked near to far. IT MEASURES SLIGHTLY WORSE and the scene's header says so: 0.141 to 0.134 over 8 songs x 8 seeds, nearly all of it layout (0.156 to 0.126). Two candidate causes were tested and both came back flat — keeping the far rows stamped scored 0.132, and running the lit surface through inkPattern scored 0.132 here and moved Effigy by 0.0009. It is the solid itself: a body always fills its own silhouette, where a stamped cast form paints anywhere between an outline and a disc depending on whether the identity is hollow, and that swing was worth three hundredths of layout variety. Kept because the score does not measure what was wrong. A crown that ignores the perspective of the field it stands in is a defect no coverage variance reports, and the header records the number and the two-line revert for whoever disagrees. The inkPattern experiment is reverted rather than left in as a plausible-looking no-op, with its measurements written where the next person will look for them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0a89676cc8 |
One scene, wide: songs against seeds
Six thumbnails is enough to rank sixty-eight scenes and far too few to study one. Worse, six songs at one seed each confounds the two inputs: a scene whose frames are interchangeable might be ignoring the identity or ignoring the music, and the library gallery cannot say which. So a scene's name in the gallery now opens it on a grid — songs down, seeds across, up to every song in the bank by ten draws. Buttons pick the size and walk onto a fresh block of seeds, which is a much better answer to "is it flat or was that ten unlucky rolls" than staring at the same ten. The state lives in the URL, so every button is also a back button. Three scores rather than one, and the two new ones are the diagnosis: ACROSS SEEDS is the same song with a different draw, so low means the scene ignores the identity; ACROSS SONGS is the same draw against different music, so low means it ignores the song. They point at different fixes. The closest pair is outlined, because at 170 cells no eye is finding it. The first version hashed the song name into the seed so a column would not be one roll repeated down the grid. It reads better and measures nothing: with the seed varying on both axes the two scores are the same comparison, and they came back within 0.001 of each other for every scene tried. A column holds its seed fixed now, which is what makes it attributable to the music — and the scores separate, with the seed moving every scene tried more than the song does. 170 cells in 9s. The songs are the slow part and are analysed one at a time so the count moves, then cached for the session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
89277705c4 |
Nothing is held for two minutes, not even one visual
planShots skipped cut planning entirely when a section had a single visual to show — "nothing to cut to" — and the exemption swallowed the ceiling with it. Adding a scene shifted the casting enough to land a 150 BPM track in that case, and it held one image for 151.98 seconds against a 22-second limit. Any new scene could have exposed it; the shot-length gate had simply never met a single-visual section before. The premise was wrong rather than the arithmetic. A shot boundary changes the FRAMING as well as the image, so the same visual filmed again at another size is a shot, and the ceiling is about how long one image is held rather than about how many images a section has. Single-visual sections now plan shots like any other. What the single case still suppresses is the hard cut: cutting straight between two framings of one image is a jump cut, so those boundaries always dissolve. Two intermediate versions were wrong in ways worth not repeating: splitting the section into equal spans put a 23.5s shot past the ceiling once the cuts snapped to downbeats, and read as a metronome (cv 0.048) to the rhythm check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
144c8c918e |
The song brings a body, not only an outline
A silhouette is the same picture from every angle, so a scene that turns one is
showing you the same shape rotated. That is the ceiling the cast has been under:
it can be notched and hollowed and it still cannot be walked around.
So the identity generates an ASSEMBLY — two to six parts, each a primitive with
an offset, a scale, a rotation and a boolean op, all under a symmetry. The
symmetry is the load-bearing half: parts unioned at random positions read as
debris, the same parts folded read as designed, and only a designed object is
worth calling a protagonist. Prism parts are the existing 2D profile extruded,
so the solid and the silhouette stay one character rather than two generators
running side by side.
It travels as data like every other artifact: three vec4 rows per part in
u_formPart, plus the scalars. Scenes declare `consumes: ['form']` and get
castSDF3, castMarch, castSolid, castChorusSolid and castLit; with no identity
they fall back to the flat profile extruded, so the helpers are safe to call
unconditionally. The chorus is the same rows with fewer parts and its own
proportions — a relative, not a second generator, and no extra uniforms.
Three scenes carry it. Effigy is new and holds the object still while it turns.
Floating Geometry and Swarm were already loops of stamps and are now loops of
bodies; Swarm is what the chorus solid exists for. That is 34.8% of videos
containing a 3D cast, against 11.9% when only Effigy had it.
Measured, the outline does change rather than merely spin: over one turn the lit
area of Effigy's subject varies 14-113% against Soloist's 3-51% for the same
rotation. Whether that reaches the variety blocks is not yet measured, and
HOWTO-variety says so rather than claiming the win.
Four costs, each found by measuring rather than by reading:
* every pixel evaluated every instance's field — Swarm at 59ms/frame against a
60ms ceiling. Bounding-sphere reject first, now 12.3ms.
* instances overlap several deep at the top of the size range, and marching
all of them made Floating Geometry's own gate run for minutes. First-wins
instead of last-wins, which was arbitrary either way.
* a normal inside the march loop multiplies four copies of the SDF by the step
count, because GLSL unrolls a fixed bound. Hoisted out.
* the helpers in the shared preamble made all 68 scenes compile what 3 of them
call. FORM_PREAMBLE is appended per scene instead.
The lint's backtick check was green through two of my own breakages: quoting a
name in a doc comment adds backticks in PAIRS, so parity survives and the
pair-scanner just re-partitions the file. It now finds where a shader literal
opens and requires the next backtick to be a real terminator — which
immediately found a second stray pair.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
518e9ea731 |
Epic 4: record why direction was negative, and what is left
The first measurement offered three readings for a direction of -0.14. The third one — the arc is not reaching the image — was correct, and it is now +0.24 against an arcless reference of -0.02, with the floor rising exactly as §8 predicted. Records what the two dead channels were, since neither was visible from the source and both passed every gate that existed: the overlay path had an empty candidate set (0/144 stacks), and the camera moved the frame by a median of 0.029 of a half-frame at a random angle so successive shots cancelled. Both cleared their checks because every bound on them was a ceiling. A gate on a device needs a floor. Also ranks what remains, with the broken seed-variety instrument first: three of the four open items are measurements and they all report through it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
eda8ac3810 |
Composable vs canvas: say which scenes are meant to sit on top of others
The top of the gallery is full of black frames with a bright element on them. That is genuinely varied and thin to watch, and the reason it ranks is that the gallery previews every scene ALONE — which is not how anyone watches one. Measured, the split is not marginal: 37 of 66 scenes paint under 30% of the frame, and the sparsest are exactly the ones topping the ranking. Storm Rift and Pendulum Trace paint 0%, Droste Feedback and Spectrum Sculpture 2%. At the other end Block Mosh covers 98% and Scan Tear 96%. So a scene now declares a `surface`. A canvas fills the frame and belongs underneath; two of them stacked is two pictures fighting rather than one picture with depth. A composable scene is mostly empty by design and reads as elements ON something. The distinction was implicit in `role: 'accent'`, which marked the depth passes and said nothing about the sixty scenes treated as interchangeable backgrounds while half of them were sparse. Verified rather than trusted, in the pattern the rest of the project uses: a gate renders every scene and measures what it actually paints, failing a composable that covers more than 55% — it would hide whatever it sits on — and a canvas that covers less than 8%, which is a canvas in name only. The gate also prints the whole coverage table, since that list is how the library gets labelled in the first place. Casting uses it: only composable scenes are drawn as overlays, and because what goes on top is now guaranteed to leave the shot underneath visible, layering happens far more often — the overlay chance went from 0.12 to 0.3 at its base. The gallery shows coverage and surface beside each score, sorts by sparsest, and says in the header why the two numbers explain each other. Nothing is labelled `composable` yet. Every scene keeps the behaviour it had through `surfaceOf`, which defaults to canvas unless the scene is an accent, so this commit changes the mechanism and not the output. The coverage table is what the labelling should be read off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c099501ead |
Raise the variety bar to 0.1
Deliberately a target rather than a description of where the library sits. Of the scenes measured since the region block went in, only Droste Feedback at 0.324 is comfortably over; Plasma Bloom is 0.082, Voronoi Shatter and Apollonian Gasket 0.062, Moiré Grid 0.047. Most of the library fails this, which is the point — a bar set where the work already is measures nothing. Third value in two days, and both previous moves were instrument corrections rather than changes of mind: the motion block was reading zero for the entire library, and adding the region block made the total a mean over six blocks instead of five. Both raised every score, so 0.04 had quietly drifted down relative to the thing it was judging. Documented as something to re-read off a fresh gallery whenever the descriptor changes, rather than a number to carry forward. The marker now sits at the middle of each meter and the grades land where they should: 0.047, 0.062 and 0.082 all red, 0.140 amber, 0.324 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b879abe230 |
A region block: how differently the parts of a frame behave from each other
The descriptor could say where a frame's energy sat and not whether one part of
the picture was doing something different from another — and that second thing
is what "nothing to watch" means. A pattern spread evenly over the screen has no
subject precisely because every region is the same region, and `layout`
normalises that away: a uniform field and a field with a form standing in it can
come out at nearly the same distribution.
So each ninth of the frame is now characterised in its own right — how much
detail, which way it runs, how many elements — and expressed as its deviation
from the frame's average. Heterogeneity rather than level, so a brighter or
busier frame does not register as a more varied one.
It is immediately the strongest block in the descriptor, and it credits scenes
the old one was underrating:
Apollonian Gasket 0.039 -> 0.062 region 0.180
Plasma Bloom 0.053 -> 0.082 region 0.182
Moiré Grid 0.037 -> 0.047 region 0.097
Droste Feedback 0.219 -> 0.324 region 0.446
Apollonian Gasket is the interesting one. It was called out as looking good and
scoring badly, and the answer turns out to be partly that the instrument was
missing the axis it is good on rather than that beauty and variety are simply
different things. Both were true; only one of them was the instrument's fault.
The metric's own gates still hold, which is the condition for believing any of
this: recolour moves structure 0.019 while moving colour 0.723, a quarter turn
moves it 0.0000, and the same scene against itself is 0.0000 against 0.0992 for
two different scenes.
EVERY SCORE HAS MOVED. The total is a mean over six blocks now rather than five,
and the 0.04 bar was derived under the old one. It needs re-deriving from a fresh
gallery before it is used to judge anything.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
8defef3ed0 |
Tell a dead shader apart from a boring scene in the gallery
A failed shader compile renders black, and black frames are identical to each other, so every block scores 0.000 and the row reads as the least varied scene in the library rather than as a broken one. That happened yesterday — the subject helpers were declared above the ink they call, GLSL has no forward declarations, the whole preamble failed to compile, and the gallery reported a variety of exactly 0.000 with no indication anything was wrong. It is a particularly bad failure mode for a tool whose entire job is ranking scenes by that number. A scene whose first frame has no luminance and no variance is now reported as broken, with both figures and a pointer to the console, and the row is coloured apart from the merely flat ones so it cannot be mistaken for a bad score. The summary counts them separately. Verified against both answers rather than only the happy one: a healthy Moiré Grid comes back 0.0365 with no error, and a deliberately broken copy of the same scene comes back with the diagnosis instead of a zero. Recorded in HOWTO-variety.md alongside the other instrument traps, since the general lesson outlives this instance — a broken render produces the most boring possible numbers rather than an error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c950f8f3b1 |
Give a full-frame field a subject the eye can track, and let the song choose
what it does "One pattern across the screen" scenes have nothing to watch. Warping the field around a focal point was the previous attempt and it did not fix that — measured on Voronoi it raised orientation variety while layout stayed at 0.004, which is another way of saying the cells changed and the frame still had no subject. So a subject is now a form that does something TO the field, and which thing is the song's decision rather than the scene's: shift, warp, punch, morph or overlay. The same form punching a hole, bending the pattern or running it at another rate are three different videos, and across six songs the bank picks overlay, morph, shift, morph, warp and punch — so it is a real axis rather than a constant with five names. subjectSDF, subjectMask, subjectEdge and subjectWarp are in the contract, so any field scene can take a subject without knowing where the focal points are. Moiré Grid is the pilot and implements all five impacts. 0.031 to 0.037 — up a fifth, and the layout block moved from essentially nothing to 0.018, which is the first time anything has moved layout on a full-frame field. That is the number that speaks to the complaint: the frame's energy is no longer spread evenly, so there is somewhere to look. It is still under the 0.04 bar, and I have not seen it. The score says there is now a subject; whether it is worth watching is the question the gallery cannot answer. One bug worth recording: the subject helpers were placed above the ink in the preamble, and subjectEdge draws with inkStroke. GLSL has no forward declarations, so the whole preamble failed to compile and every scene using it rendered pure black — which the gallery reported as a variety of exactly 0.000 across all six blocks rather than as an error. An all-zero row means a dead shader, not a boring scene. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
10eeddcf6d |
A guidance document for building visualizers that vary
HOWTO-visualizers.md gets a scene working. Nothing said how to make one look different from song to song, which is the thing the library is worst at and the thing every measurement this epic has been about. Written as evidence rather than advice: each claim carries the number behind it, so a later change can contradict it. Several already contradict things believed earlier in the same week. It records the failures at least as carefully as the wins, because they were more informative and each looked obviously right beforehand — effects as parameters inflating a score without changing a picture, concentrating disturbance instead of adding it, the roster-size theory that a direct sweep found to be nothing, and reading a form as a metric rather than drawing it. It also documents what the descriptor cannot see, since half of "why is my score low" is there: brightness, colour, rotation, quality, and layout for anything that fills the frame. And it is explicit about which numbers to trust. Direct render comparisons have a noise floor of zero; aggregate ratios swing enough to have produced four withdrawn conclusions in one epic. Ongoing by design, with the open questions listed: coupling has never moved off zero, fixed-geometry scenes have no known route to variety, and the 0.04 bar was set against scores that were depressed by a measurement bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5a9dfa6b2b |
A scatter point in Isometric Blocks — which measures worse, twice
Asked for, built, and honest about the result: the focal point costs this scene
a quarter of its variety and I could not make it pay for itself.
uniform scatter, no focus 0.0490
focus, trading scatter for it 0.0374
focus, added on top of it 0.0365
The first version spent the scatter budget near the focus and calmed the rest,
which put most of the field back on the rigid lattice and took the orientation
variety with it, 0.164 down to 0.105. That diagnosis looked right, so the second
version made the focus additive — every cell keeps its full scatter and the ones
near the point get more. It changed nothing: 0.0365, orientation still 0.104.
So the diagnosis was wrong and I do not have the real one. The remaining suspect
is the height modulation, which lifts blocks toward the focus and may be
flattening the skyline's variety between songs by dominating it — but that is a
guess, and two guesses have already been wrong here.
The feature is committed rather than reverted because the metric does not
measure what was asked for. A scatter point is a compositional idea, and the
gallery scores how much a scene changes BETWEEN songs, which is a different
question — Apollonian Gasket makes the same point from the other direction. This
may well look better and score worse. It needs eyes on it before the number
decides.
`gather` is a parameter and seed-driven like any other, so the amount varies per
song as asked; the focal points themselves come from the identity.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b1e58a6603 |
A focus: somewhere for a full-frame field to be about, tried on Voronoi
Edge-to-edge textures have no composition to vary. Two songs of Voronoi Shatter measured 0.004 apart in the layout block, and that was honest rather than a metric failure — the cells changed and the arrangement did not, because there was no arrangement. So the identity now decides a FOCUS: one to three points, a reach, and a pull that either draws the field in and densifies it or opens a void. The points come off the same lattice everything else is placed on, so a scene using them is composing in the song's terms rather than inventing a centre of its own. focusField and focusWarp are in the contract, available to any field scene that wants somewhere to be about. Voronoi Shatter is the pilot. It tiles in the warped coordinate, so cells crowd toward the focus or pull away from it, and its seams tighten where the field gathers so the effect reads as a change in the shatter rather than a brightness blob laid over one. 0.0463 to 0.0588, comfortably clear of the bar. It did NOT work the way the hypothesis said. The gain is in orientation, 0.140 to 0.194, and in texture; the layout block moved from 0.004 to 0.007, which is still nothing. Warping where the cells sit changes what they look like without changing where the energy is, because the field still covers the frame corner to corner. Layout stays blind to this family until a field is allowed to actually fall away from its focus and stop being full-frame — which is a bigger change to what these scenes are, and worth deciding rather than sliding into. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8d9b8a593a |
Fix a permanently-zero motion block, and let the song arrange Isometric Blocks
Investigating three pieces of feedback turned up a measurement bug underneath
all of them. The gallery built four of the descriptor's five structural blocks
and never rendered a second frame, so `motion` came back 0.000 for every scene
in the library and dragged every score down by a fifth. It read as a property of
the scenes; it was a missing render. Every number in the gallery so far,
including the ones the 0.04 bar was calibrated against, was depressed by it.
Plasma Bloom 0.0532 -> 0.0624
Voronoi Shatter 0.0431 -> 0.0463
Apollonian Gasket 0.0340 -> 0.0388
The Plasma Bloom versus Voronoi Shatter oddity is real and is the metric's
limit rather than a mistake. Plasma Bloom is a centred subject whose position
moves between songs, so its layout distance is 0.096; Voronoi Shatter is
edge-to-edge cells, and a full-frame texture has the same layout however its
cells fall, so its layout distance is 0.004. The descriptor cannot see "the
cells are different" as composition, and for full-frame work the layout block
contributes almost nothing. Worth knowing before the bar is used to judge that
family.
Apollonian Gasket scoring low with good-looking frames is the gallery working:
it measures how much a scene changes between songs, not how good it looks. A
scene can be beautiful six times and identical six times.
Isometric Blocks gets the displacement it was asked for. Every block sat exactly
on its lattice slot, so the plan of the field was the same plan in every song
and only the heights moved — a layout distance of 0.005. `scatter` steps each
cell off its slot by a fixed amount of its own, bounded under half a cell so
blocks do not cross and break the isometric read, and it drives the block's
height and side wall as well as its ground position. 0.034 to 0.049, clearing
the bar, with orientation variety nearly doubling as the rigid lattice softens.
Its layout distance stayed at 0.004, for the same reason Voronoi's did: the
field fills the frame either way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
71ddd37711 |
A minimum variety bar in the gallery, drawn as a red line
A visualizer that looks the same wherever it appears does not just fail to vary — it leaks between songs, because it is cast into many of them and the viewer who watches two videos ends up recognising the shot rather than the song. That makes the floor a property of the scene, so the gallery now states it and the scene has to clear it. 0.04 to start with, and honestly a starting bar rather than a derived one: set where the flat cluster measurably sits, to be raised as scenes are lifted to it. Drawn twice, because the two readings are different questions. Every meter carries a red marker at the bar, so a single row can be judged on its own. And in the default sort the list is cut by one red line where the scores cross it, which answers the browsing question — everything above that line repeats itself between songs. Also restored progressive drawing, which the caching rewrite had quietly lost: every draw had moved to the end of the build, leaving three minutes of spinner and nothing to look at. The first rows are the interesting ones, since the sort puts the repetitive scenes on top. Verified by seeding the cache with rows spanning the bar and reloading: four below, four above, the line between them, the marker at 20% of each meter, and the grade colours switching at the right place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
e90951c277 |
Cache the gallery against a fingerprint of the source that built it
Three minutes of GPU work is fine once and intolerable on every page load, so the gallery now builds once and comes back from IndexedDB after that. The key is the part worth getting right. A cache you clear by hand is worse than no cache: it will eventually show last week's pixels while you are judging this morning's change, and it will look like a render, so you will believe it. Here the key is a fingerprint of every file under src/, collected through import.meta.glob so it covers the shaders, the identity, the look generator, the engine and the descriptors without naming any of them — the file that invalidates a render is exactly the one nobody remembers to list. Edit anything and the entry stops matching and the page rebuilds without being asked. Thumbnails are WebP blobs rather than raw pixels: sixty-five scenes at six frames of 256x144 is 57MB raw, and measured, the compressed form is 4.1MB for the whole library. Saving a build evicts every other one, so the store cannot grow without bound. The spinner is a CSS animation on purpose. Building blocks the main thread in bursts and a JS-driven spinner would freeze mid-turn, which reads as a hang exactly when the page most needs to look alive. Verified rather than assumed, six behaviours: the fingerprint is stable across calls; a build round-trips through storage and decodes to a real image (51% non-black); a different fingerprint misses; saving a new key evicts the old; and appending one comment to one scene file moved the fingerprint from 125-f842b05e to 125-c056d7b9, which is the invalidation working end to end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c6d96c9d40 |
A gallery, and a debug index to reach it from
Sixty-five visualizers, six renders each, one per song — each carrying that song's whole identity: its cast, ink, lattice, palette and a fresh parameter draw. Side by side, a scene that cannot be changed by the song is obvious in a way no aggregate could show, which is the complaint this answers. It scores as well as shows. Each row carries the mean structural distance between its own six frames, on the same descriptor the variety harness uses and with colour excluded, so six palettes cannot disguise one image. Sorted least-varied first, because browsing sixty-five scenes hunting for the repetitive ones is precisely what a sort order should do for you. The result names names. Thirteen scenes barely change across six songs, and they fall into two groups that were already known separately. The ink-only migrations — Analog Wow, Halftone Misprint, Pitch Shatter, Block Mosh, Scan Tear — are the shallow tier flagged in MIGRATION.md, where the whole migration was one posterisation. And Moiré Grid, Isometric Blocks, Quasicrystal, Truchet Fold, Voronoi Shatter and Apollonian Gasket are the structural twin cliques the library sweep found weeks of measurement ago, arriving here by a completely different route: the sweep compared scenes to each other, the gallery compares a scene to itself, and they agree on the same offenders. debug.html collects the tools, since there are now enough of them that knowing which to open is its own problem. It also carries the two things a newcomer would otherwise learn the hard way: which measurements are safe to steer by (direct render comparisons) and which are not (aggregate ratios), and what to do when a page renders perfectly and does nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4217cca3d3 |
Rename clicktrack.js — a content blocker was eating it and killing the app
A file called `clicktrack.js` matches the click-tracking telemetry patterns that EasyPrivacy and similar lists block by substring. In a browser with a content blocker the request never completes, and because it is a module import that takes the entire graph with it: main.js never runs, no handler is ever bound, and every control in the UI sits there looking correct and doing nothing. The failure is unusually expensive to diagnose because everything else looks healthy. The dev server returns 200 with the right MIME type, curl fetches it fine, node imports it fine, and the app loads perfectly in any browser without a blocker — which is how it passed every check here. Only the console names it, and only as one line about a module that failed to load. Renamed to metronome.js, which is also the better name for what it does. The button id went with it, since cosmetic filter rules can hit ids too. The general rule, recorded at the top of the file: anything shipped to a browser and named like tracking will be treated as tracking. Avoid click, track, analytics, pixel, beacon and ad in filenames and URL paths, however honest the code behind them is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4267c588aa |
Fix two checks the migration broke, and re-aim the signature gate
Phase 2 kept its own list of contract uniforms and never learned about the identity artifacts, so Prism Bloom reading u_castSides looked like an undeclared uniform. It now takes the list from the contract itself. Phase 9 demanded that every cast scene honour the track's signature, which was true when the signature was a hard filter and stopped being true when it became a weight. The check was asserting the old contract against the new generator. Rather than delete it, it now checks the claim that actually matters. A track may reach outside its signature — that change was made deliberately, because the filter was disqualifying a third of the library and funnelling eleven scenes into half of all videos — but it must still LEAN on it. Honouring scenes have to carry the majority of the cast against a chance baseline near 25%, and section anchors, which open a section and return most often, have to honour it almost always. 89/89 checks pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
df1ce9a957 |
Withdraw the separation figure: it is a ratio of two small differences
Three runs of the post-migration song variety measurement give separation 0.314, 0.555 and 0.065 — mean 0.311, half-range 0.245. The 0.38 quoted from a single run was meaningless. The cause is structural rather than a sample-size accident. Separation is (observed - floor) / (ceiling - floor), and here the numerator is 0.0089 while floor and observed each carry an error bar of 0.015 to 0.019. Dividing one small difference by another amplifies the noise in both. The metric cannot resolve the thing it was built to report. What survives: the ceiling now sits above the floor on every run, with zero variance, where before the epic it landed underneath and the ratio was not computable at all. That is a change in kind and it is solid. The magnitude is not. The decomposition is the measurement to steer by — noise floor of exactly zero, effects an order of magnitude above any drift — because it compares renders directly instead of dividing differences of aggregates. Documented as such. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c3b67f45bd |
Record the migration's end-to-end numbers
Song separation went from not computable — the reference sat below the floor — to 0.38, with the floor falling from 0.1449 to 0.1185 and the gap widening. Every structural block moved: scale 40 to 59%, orient 65 to 89%, layout 60 to 111%, texture 89 to 81%, motion 53 to 65% of the reference. Container variety rose throughout, 0.0557 to 0.0815 to 0.0901, which is the stop condition holding: the library did not homogenise as it converged on shared content. Coupling did not move and is recorded as such. Whether a song looks different in proportion to how it sounds different is still unsolved, and the migration was never aimed at it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
572b090b83 |
Fix two scenes that were failing their gate before the migration touched them
The library-wide gate run left two failures after the migration was complete, both of the shape "renders nothing at the bottom of a param range". Running the same gate against the pre-migration source shows them failing identically, so the migration did not cause either — it exposed them. The per-scene battery is opt-in and nobody had ever run it across all sixty-five at once. The defect is the RANGE, not the shader. Spectrum Sculpture at radius 0.15 is too small to register and Circuit Bloom at grown 0.25 has no pads yet, and both values are ones the generator is free to sample. Floors raised to where the scene actually draws something. All sixty-five now pass the full battery: renders, animates, deterministic, distinct, param sweep, flash rate, every declared trait and every declared artifact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
00ad1d8c2b |
Measure where visual difference comes from, and correct the epic's framing
Two cheap measurements settled a question three expensive ones had not, and both
of them contradicted the diagnosis offered for it.
The stated diagnosis was that the identity's expressive range had become the
bottleneck. It has not. A census of the identities themselves — no GPU, seconds
to run — puts mean distance between twelve songs at 0.43 with no near-identical
pairs and full coverage of every decision space: six of six fills, five of five
lattices, six of six protagonist forms, three of three element scales. The songs
are handed genuinely different designs.
The decomposition then asked whether those designs reach the picture, by holding
the container fixed and varying only the identity, then the reverse:
identity only 0.0299
container only 0.0557
both 0.1101
neither 0.0000
Identity is worth 54% of what the container is worth, against an instrument
noise floor of exactly zero, and the two compose to more than their sum. The
inversion works at the frame level. What it does not do is replace the container.
That corrects EPIC-3 §5, which proposed a song picking two to five stages on the
theory that shared content would substitute for container variety. Container
variety is the larger of the two effects and identity adds to it. Four stages
with a rich identity throws away the 0.056 the library was already providing —
which is exactly the shape of every measurement in this epic: stages have the
lowest floor of any arm and no advantage in spread.
The direction is therefore not a small set of stages. It is the whole library
consuming the cast: keep the sixty-one containers and make them draw the song's
content rather than their own. The migration was filed as a cost; it is the
payoff.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
6e4106048b |
Staging: a shared lattice, and the element size the songs were all sharing
The cast and ink slice lowered the floor as predicted but left `scale` — feature
size — consistently WORSE than the legacy arm, 0.033 against 0.053 and well
outside the noise. Every stage still chose its own element size from its own
param range, so every song landed in the same place: the videos shared a cast
and also, accidentally, shared how big everything was.
Staging is the third artifact. A lattice — grid, radial, spiral, scatter,
strata — with jitter, spread, a size hierarchy and a bias toward the middle or
the edges, transported the same way the cast is: uniforms plus a `stageNode`
function in the preamble, no new engine plumbing. Constellation, Swarm and
Procession place on it; the stage keeps its motion and gives up its composition.
Soloist takes only the scale, since a close-up has no composition to share.
`elementScale` is the part that mattered. It is the song's answer to "how big is
this made of", spanning about a factor of six, and it is the decision that was
missing rather than mis-set. Measured, over twelve songs and three runs:
stages spread +0.0111 ±0.0043 → +0.0143 ±0.0008
scale block 0.033 ±0.004 → 0.041 ±0.007
Against the legacy arm at +0.0108 ±0.0071 the harness still says
indistinguishable, and it is right to: the gap is +0.0035 and the legacy arm's
own run-to-run range is twice that. What can be said is narrower and holds up.
Stages have the lowest floor of the three arms by a clear margin — 0.082 against
0.095 and 0.104 — so sharing content does make a video look like itself, which
was the central prediction. And the targeted fix moved the block it was aimed at
in the direction it was aimed.
Also added: a lint check that the shader preamble contains no backticks. Twice
now one has closed the template literal and produced a check page that hangs on
"starting…" with an empty console, which is an expensive way to find a typo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
577ec107f6 |
Sweep pool size directly, and withdraw the claim that it mattered
The Epic 3 arms appeared to show that a small casting roster was the largest available win. Swept directly across 4, 8, 16 and 32 over twelve songs with three pool draws each, the differences are 0.003 to 0.007 against a run-to-run noise of +/-0.003 to +/-0.005. Pool size does nothing measurable. The arms varied two things at once — smaller pool AND the same pool for every song — and only the stages-versus-legacy comparison inside them held everything else constant. That one still stands at +14%. POOL_SIZE stays at 8, now on grounds the metric cannot see: about nine distinct scenes in a video rather than seventeen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ec38def1a9 |
Epic 3, first slice: the song brings its own cast
Four stages, an identity layer, and an A/B that says the idea is right and the
reason it works is not the reason I expected.
A stage has no image of its own. It owns arrangement — a procession, a
constellation, a soloist, a swarm — and what it arranges comes from the track:
`castMain` and `castChorus` for the forms, `inkMask` and `inkValue` for the hand
they are drawn in. The identity generates a protagonist and a chorus with sides,
notches and hollows, plus an ink treatment of weight, edge, fill, outline and
posterisation. All of it travels as uniforms, so it is data rather than code and
a stage consumes it without knowing any other stage exists.
The protagonist IS the signature form rather than a second opinion about it.
They were separate draws in the first version, which let a track built on
hexagons put a round protagonist on screen — the signature said one thing and
the picture said another, and the shape trait stopped meaning anything for
stages. The cast now reads its geometry from the personality live and adds the
notches and hollows that turn a shape into a character.
Measured across seven songs, three arms, same instrument:
arm floor observed spread
stages, four of them 0.0801 0.1133 +0.0332
legacy scenes, four of them 0.0830 0.1122 +0.0292
the unrestricted generator 0.1058 0.1146 +0.0088
The prediction in EPIC-3 §7 was that sharing a cast would drop the floor
sharply. The floor did drop sharply — by a quarter — but nearly all of that came
from casting FEWER scenes per video, not from sharing content: the legacy arm,
four ordinary scenes with no cast at all, gets most of the same benefit. Stages
add about 14% on top of that.
That is worth knowing before building the other four registers. The single
largest available win was hiding in the roster size all along, and it costs one
number to take.
One result runs the wrong way and is recorded rather than explained: coupling is
+0.29 on the legacy arm and -0.18 on the stages. At twenty-one pairs neither is
distinguishable from zero, so it is a flag for a larger sample rather than a
finding.
Also fixed: a backtick inside the shader preamble's template literal, which
closed it and made every check page hang on "starting…" with no error in the
console.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
75e0f0ef01 |
Make the whole library reachable, and measure what that did and did not buy
Three changes, from the list the measurements pointed at. The personality's values now centre on the audio and the seed picks within that centre, rather than every value being drawn from the seed alone. A bright track sits high in its frame, a dynamic one has depth, noise earns corners where tonal material stays round. The signature's own choice is tilted by the music too — it decides which scenes a track can cast, so leaving it to the seed meant the most consequential decision in the generator had no relationship to the song. The signature stopped being a hard filter. As a filter it was the single largest cause of sameness: a scene declaring all four traits was eligible for every track and opened half of all videos, one declaring two was eligible for one track in fourteen, and eleven scenes out of sixty-one carried nearly everything. It is a weight now, worth six times at full honour. That fix alone made things worse, which is worth recording. The filter was doing two jobs — collapsing the library, and giving each track a DIFFERENT pool to cast from — and removing it kept the second loss. Every track drew from the same weighted library and measured song separation fell. So each track now draws its own pool of about a third of the library, weighted by the signature but sampled without replacement, keeping the differentiation and dropping the bias. Motion became a character rather than a rate. Tempo was the only lever and tempo compresses; stillness is a separate question from speed, and it can now halve the animation rate or raise it by a third. What it bought, measured against the same instrument: every scene in the library is now cast, where sixteen were never reached; identical casts across seeds went from four pairs to none; and the raw structural distances all rose — motion by 57%, layout by 22%, scale by 23%. What it did not buy: videos also became more varied INTERNALLY, by more than they became different from each other. Two songs are no more distinguishable relative to how much one video already changes over its own length than before, and the coupling between musical distance and visual distance is still indistinguishable from noise at this sample size. The ceiling reference now sits below real outputs, so the separation ratio is reported as not computable rather than as a large number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a946f0e105 | wip: instrument guards | ||
|
|
2bbf5bcf42 |
Run both variety tests against the song bank, and fix the ceiling twice more
The tests now run on real bank entries. The seed gates used to run on a two-section synthetic whose only kinds were intro and outro, so half the scene library was unreachable and the number was measuring that rather than the seed. Probes are labelled by section kind and occurrence rather than by index, which is what makes two different songs comparable at all — they have different section counts, so probe 3 of one is not probe 3 of the other, and matching by position would compare a drop against an outro and score the mismatch as variety. For two seeds of one song the labels are identical and this changes nothing, which is the point. The song test measures one thing the seed test does not: coupling, the rank correlation between how different two tracks sound and how different their videos look. Separation alone can be had by a generator that ignores the audio and hashes the file, and that would be a perfect score for a completely wrong video. Separation without coupling is not variety, it is a different seed per file. The ceiling took two more attempts. Recasting every layer at random averages a dozen scenes together and a dozen random scenes converge on the same generic busy image, so two references came out closer to each other than two real videos and blocks scored over 100% of achievable. Forcing one scene per reference collapsed the other way: a video that never changes scene has almost no internal variation, so the ceiling landed BELOW the floor, which is a within-video quantity. A reference has to match the structure of what it bounds. They now keep the real pipeline — rosters, shots, per-section sampling, so a reference rotates between three or four scenes exactly as a real video does — while drawing from disjoint slices of the library. Same complexity, nothing in common. The fingerprint measurement had the same shape of error: pooling every probe mixed in how much each video varies over its own length, which is large for everything, and washed the answer to a flat 100% while the separation score said almost everything was collapsed. One vector per video now. Both tests fail as committed. Seed separation 0.04, song separation 0.03, coupling -0.03 — two different songs differ from each other by about as much as one video differs from itself, and that difference has no relationship to the music. Colour scores 112%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e0c0dd664a |
Give the sections shapes, and the songs an opinion about their own edges
Two complaints, one cause: stages were flat blocks and the noise bed was glued to the pad level, so the bed was a constant hiss under a track that never went anywhere. Sections have contours now — rise, fall, swell, dip, surge — sampled per note rather than per stage, so a build builds through its chords instead of stepping between two flat halves. The noise gets its own per-stage level instead of following the pad, which makes it an arrangement element: a riser through a build with its filter sweeping up, a wash under a drop, nearly absent in a breakdown. That is also the better test signal, since the segmenter classifies a section partly on its energy slope and a build that does not build is one it has to guess at. Transition hardness is a per-song axis. Genres differ on this more than they differ on tempo — an ambient record dissolves between its sections and a club record cuts — and until now every song in the bank cut. Soft songs crossfade across a couple of bars; the hardest get the pre-drop trick, where everything stops for most of a beat before the loud stage lands. Five soft, three mid, nine hard, and the builder fails if the bank ever loses either end. The contours immediately broke the axis they were layered onto: section-to- section contrast is dynamic range, so adding it put a floor of 0.50 under a statistic that had reached 0.18. That is what compression IS, so contour depth now scales with the track's dynamics — a limitered master has shallow section contrast as well as a shallow crest. Back to 0.29, which is as low as enveloped notes and hard cuts will go. One regression accepted rather than fixed: the soft fades cost the beatless entries their tempo detection, since the attack that gave a drone a pulse is exactly what a crossfade removes. `drone` reads 170bpm for a 62bpm source. A beatless track has no tempo and the detector is guessing either way; `ember` exists to anchor the low end with a beat that is actually there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d59dd9d5dd |
Give the song bank notes instead of a test tone
The tonal content was a sustained sine stack at a fixed root, and the stage multiplier scaled that root directly — so a drop arrived at roughly 1.2kHz with ten harmonics stacked on top of it and held there for thirty seconds. Every statistic it was built to control came out correct. It was also unlistenable, and a bank nobody can stand to play is a bank nobody audits. There is a scale, a chord progression, a bass on the chord roots and a seeded motif over the loud stages. Key, mode and progression come off the song's seed so two entries are not the same four chords at different tempos. This is the plainest thing that qualifies as music — not trying to be good, trying to be playable enough that a person will listen to the bank and notice what is wrong with it. Register is the real fix. `stage.root` used to multiply the fundamental and now adds voices upward instead: bass and chord stay where they belong, a lead octave arrives when the arrangement opens up. Brightness still moves the harmonic content and the key's register, but across an octave and a half rather than a piercing sweep. Harmonics above Nyquist are dropped rather than left to alias, since folded energy would corrupt the centroid and flatness the bank exists to control. Two measured consequences. Tempo detection on the beatless entries got better, not worse — the bass gives a pulse where the drone gave nothing, and `drone` now reads 62bpm instead of 88. And enveloped notes put a floor of 0.27 under the measured dynamic range, because the gaps between notes are gaps a limitered master does not have; note decay flattens toward a hold as dynamics falls, which takes the compressed end back to 0.18. Coverage still passes on all six axes and all six section kinds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cfe77fd524 |
Finish the song bank: axis ranges that are real, and a collinearity check
Completes the bank whose first half went in with the scene commits. The remaining work was all in what "covered" means. Two axis ranges were wrong, and being wrong made the coverage report lie in both directions. Loudness is a raw mean spectral magnitude, not a normalised 0..1, so every song read as 0.00 and the axis looked dead. Centroid is mapped to a log frequency axis, so a track of nothing but sub-bass and a 55Hz pad still measures 0.28 and pure hiss measures 0.93 — against a nominal [0,1] the bank would have reported a permanent 50% gap that no synth change could close. Both now carry the range the statistic can really take on, with the reason. Spread on every axis is not coverage of the space, so the tool also measures correlation between axes. It immediately found brightness and noisiness moving together at r=0.95: noise colour had been tied to brightness, one axis wearing two names, leaving the dark-and-noisy quadrant unreachable. Noise colour is now its own parameter — hiss over a sub-bass pad is an ordinary record. That got r to 0.94, and no further. Flatness is the geometric mean of the spectrum over its arithmetic mean across the whole band, so a signal only measures flat if it has energy everywhere, which is the same thing as measuring bright; band-limiting the noise to hold the centroid down empties the top and drops the flatness with it. It is a property of the analyser and real material does the same, so it is allowlisted with its reason. Anything not on that list still fails — the check is there to catch a bank that has collapsed, not to relitigate physics every run. The output is gitignored. It is 172MB of deterministic audio derived from a table that will keep changing: rebuild it, do not carry it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0c5b0a8d4d |
Close out the twenty: what the gates taught
Three lessons worth more than the scenes: a slow axis has to move a large low-frequency area or the camera's own drift beats it, whole-frame luminance on the kick is the strobe the flash gate exists for and it arrives by accident, and a style trait expressed only as grain measures as no style at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
905c217193 |
Three glitch scenes: a press, a loop and a tape
Halftone Misprint is a printing fault rather than an electronic one — the image is never damaged, only separated and reassembled out of register, and nothing else in the library is made of dots. Droste Feedback scales the previous frame where Time Smear translates it, so the image never clears. Analog Wow is the continuous, wet counterpart to Scan Tear and Block Mosh: the error varies smoothly down the frame because every line was written at a different moment. Three things that had to be got right. The halftone ruling is a count of dots across the frame, not a pixel pitch, or a 4K export is the same dot on a bigger sheet. The Droste loop has to CONTRACT — expanding pushes every copy off the edge and leaves a plume instead of a corridor. And all three expressed style only through grain at first, which is the Side Quest 1 complaint: they now put the track's line weight and edge softness into the dot, the ring and the band boundary, taking the measured style response from 24/11/18 to 86/255/88. Neither feedback scene declares a slow axis, with the numbers recorded in the files: their own history moves the ten-second average further than any parameter does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
91e74ff167 |
A cast census, and the arrangement it needed to be true
"Twenty-three of sixty-one scenes are never cast" was an artefact of the song it was measured on. synthesizeSectioned has one change point, so it segments into exactly two sections and both are quiet kinds — and intro, breakdown and outro are restricted to the restful families for every director. Half the library was unreachable before a seed was drawn, and the measurement reported that as a casting failure. synthesizeArrangement builds a real one: intro, build, drop, breakdown, drop, outro, shaped to hit the segmenter's own classifier rather than to sound like anything. Measured against a bank of those, two scenes out of sixty-one are never a background, not twenty-three. The census exists because "never cast" without a reason is unactionable. A scene can die at the signature gate, at the director's family table, or in the roster draw, and those are three different repairs — a trait declaration, a table, and arithmetic that neither fixes. It reports which one, and it separates being cast as a background from appearing as a translucent overlay, because a library can look fully used while nine scenes carry every frame. What it finds is that eligibility is almost entirely a function of how many traits a scene declares. Four-trait scenes are eligible for every track and open half of all videos; two-trait scenes are eligible for one track in fourteen; the single scene declaring one trait is eligible for none, ever. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
656e062069 |
A seed variety test, and a metric that had to be gated before it was believed
Every other phase asks whether one video is correct. This asks whether two are different — the failure the suite could not see, since a generator that ignores its seed passes determinism, flash safety and liveness perfectly. Frames reduce to a structural descriptor built to be blind to the cheap axes and sensitive to the expensive ones: standardized luma kills exposure and palette, a Laplacian pyramid gives the radial spectrum, and the gradient-angle histogram is carried through a DFT magnitude so a rotation shifts it without moving it. Colour is measured and never counted; its only job is to expose the case where two seeds differ by a palette swap and nothing else. The score means nothing on its own, so it sits between two references the same instrument produced: a floor of how far one video travels from itself across its own sections, and a ceiling of the same pipeline with every layer recast at random. Two checks gate the instrument before any number from it is trusted — recolour must move structure ~0 while moving colour a lot, and a quarter turn must not move it at all. Three things this got wrong first and now does not. Averaging each video's probes into one descriptor washed out the structure being measured and put the floor above the ceiling; probes are matched instead, which is fair because the track is held fixed. Cosine distance on all-positive histograms scored unrelated scenes at 0.05, too compressed to be read; chi-square replaces it. Single-link clustering in the library sweep chained overlapping pairs into a fourteen-scene group that did not exist; complete-link means every pair inside a group is really a twin. The sweep runs against all 61 visualizations, and it finds what the per-scene distinct gate cannot, because that one compares raw pixels and structural twins are merely differently coloured. Both rendered gates fail as committed. That is the point of them: separation is 0.07 against a 0.35 target, two 4-cliques of scenes are one look each, and a third of the library is never cast. The instrument passes; the generator does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
875c3868dd |
Three geometric scenes, and two ways to strobe a whole frame
Voronoi Shatter (cells with no fixed shape, re-cut on the phrase), Apollonian Gasket (a packing built by inversion, so zooming finds more circles rather than finer noise) and Isometric Blocks (a lit, gridded, solid surface where Floating Geometry is bodies adrift). Two flash failures, both the same mistake in different clothes. Apollonian's hard depth cutoff popped a whole generation of discs in and out as the fold crossed it — now faded rather than cut. Isometric Blocks drove block height off the low end, so every tower in the frame grew and shrank together: 4/s. Moving the reaction to the edges and to a hash-chosen quarter of the tops keeps the pulse and drops the frame-wide swing. Voronoi Shatter declares no slow axis: its re-cut puts the ten-second average 0.127 from itself between any two windows, the highest noise floor in the library, and no parameter competes with that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
50ceb7afa1 |
Four structural scenes: a span, a well, an arcade and an aisle
Suspension Span is two curves and a rhythm of hangers where Girder Lattice is a texture of members. Stairwell Descent puts the vanishing point dead centre and turns each flight, so the spiral is in the structure rather than in the motion. Aqueduct March is masonry — the light comes through holes cut in a solid wall — and Data Aisle is the interior, close counterpart to Neon City's exterior. Two bugs worth naming: the arch openings compared a cell-local centre against a global coordinate, which put every arch out in the wings and left a blank wall; and the stairwell's depth fade was keyed on floor index, so it only darkened the few pixels at the centre and measured as doing nothing. Aqueduct March declares no slow axis and records the three candidates measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c8658a6d15 |
Four minimal scenes, and a transition control that was reading its denominator
Pendulum Trace (a harmonograph over its own fading ghost), Contour Map (a survey of a landscape with a tide in it), Shoji Grid (backlit panels that slide) and Balance Stack (a leaning pile under a crossing sun). Two of the four leave the slow axis undeclared and say why in the file: in a frame this empty the camera's own drift moves the ten-second average more than anything in the scene can, so a declaration would be a claim Phase 11 contradicts. Phase 4's transition control changed from max(before, after) to their sum. These scenes are calm, so casting them into the mid-shot control windows halved the denominator while the boundary peak stayed put — 0.124 with them, 0.130 without. For most of the window both stacks are live, four layers with accents in overlay and screen, and a nonlinear blend of two moving stacks moves more than either alone; "no worse than the busier half" was never a property a dissolve had. A real pop still clears the threshold by an order of magnitude. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b01e3aff6f |
Three organic scenes: a reaction, a mat and a surface
Turing Bloom runs activator-inhibitor in the feedback buffer, so the pattern is formed rather than drawn — the one organic here that will not go uniform. It declares no slow axis on purpose, and says why: its own convergence path moves the ten-second average by 0.10, and every candidate axis measured under that. Mycelium Web puts an organic on the ground instead of standing in front of the camera; the colony front is legible as area, which is also what makes its axis measurable. Scale Mosaic is a log-polar lattice with a shear, so the rows are spirals and every scale is the track's signature form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
10a4cff0f6 |
Three flow scenes with a still point in them
Rain Column, Magnet Lines and Kármán Street. The flow family was seven ways of advecting a noise field; none of them had an upstream, an obstacle or a rule. These do: rain falls in three parallax layers under one shear, the field lines are exact contours of the poles' stream function so they close on themselves, and the vortex street is shed by a body that never moves. The slow axes took four rounds to land. A fine-grained axis — line count, drop density — loses to the camera's own drift in the time-averaged comparison Phase 11 makes, so all three ended up on a param that moves a large low-frequency area: streak length, pole spread, and a wake haze that dissolves the streamlines it widens over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
47e6fa33f9 |
Twenty more scenes: the list, and why these twenty
Even spread across families is not useful spread — casting disqualifies any scene that misses a signature trait, so space and shape are worth more than another entry in a full family. Each scene is specced against the neighbour it must not resemble, because that is the gate with the numeric floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e6f5a2f0d5 |
Epic 2.5.2: make the framing actually reach the image
Revisiting the framing layer turned up that it was not reaching four of the forty-two scenes, and that none of its gates could have told us. Those gates check the PLAN — cue sizes, the distribution of shot sizes across a population, headroom, seek determinism — and a plan that never reaches the image passes every one of them. Rendered at wide, normal and close, Scan Tear, Pylon Grid, Pitch Shatter and the 3D Particle Field came back byte-identical at every size. The cause was a category error in the first implementation. Framing was applied inside sigCamera, which is gated on the `camera` personality trait — so a scene that declined the track's drift and sway silently declined the shot size as well. That gating is right for a TRAIT and wrong for framing, which is not one: framing is where the camera is standing for this shot, and no scene should be exempt from it because of an unrelated art-direction decision. - Framing now lives in the shader epilogue, applied to the coordinate every fragment scene is handed, so honouring it is not optional. uv is left unframed on purpose: it is screen space, and prev() and sigGrain belong to the output image rather than to the scene being filmed. - Scan Tear and Pitch Shatter build their image from uv deliberately — a signal artefact happens to the signal, not to the world behind it. They now slice on raw uv and build the field they displace from a new framedUv(p), so the tear stays locked to the frame while the imagery behind it is filmed wide or close. - Particle Field receives framing in update() and honours it as a camera dolly, which is what framing literally is when a layer has a real camera. Distance divided by scale, matching the fragment path where the coordinate is divided by it. New gate renders instead of inspecting: 42 of 42 scenes now respond to framing, weakest Ridge Terrain at 0.22 of its own brightness, against a 0.05 floor. Also drops a stale comment on Layer.setFraming that still claimed sigCamera applied it. 105/105 checks pass including the slow set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c92c398d18 |
Drop the scratch probes, and stop them coming back
_audioprobe, _captureprobe, _decodetest, _encprobe, _exporttest, _muxprobe
and _playtest were one-off harnesses for debugging the decode/encode/mux
path. They were untracked working-tree files until `git add -A` in
|
||
|
|
8f5ff3a239 |
Side quest 2: give three churning scenes something to develop
Curl Flow, Signal Decay and Circuit Bloom had no parameter that changed their structure. Each was one process at one scale, statistically identical everywhere and at every moment — which is the technical statement of "it looks the same for five minutes". Every pixel moving, the image never changing. Each got the large-scale structure it was missing, and the slow axis now has something real to walk: - Curl Flow gains a CURRENT: one broad band carries the filaments and the rest runs bare. The axis is the band's strength rather than its position, and that choice was measured — moving the band preserves the frame's total energy, so a ten-second average of it is nearly the same image wherever it sits (0.0028 across a full sweep). Opening and closing it changes how much of the frame is lit at all. The current gates the final image rather than only the vein term, because the feedback trail and the flow tint each fill the frame on their own. - Signal Decay gains a DAMAGE FRONT: lock falls away on one side of a moving boundary, so the stack has a shape instead of every lane being an independent coin flip. Hiss had to be gated by it too — hiss rises as lock falls, so without that a damaged lane simply traded signal for noise and carried the same energy, and the front cancelled itself out. - Circuit Bloom now actually BLOOMS. `reach` was a fixed vignette, so the packets ran, the pads blinked and the board never changed. Its range stops at 1.6 because the frame's far corner is ~1.4 units out and travel spent past that changes nothing — the first attempt wasted most of the axis up there. The gate itself was wrong, and this is the more important half of the commit. It compared the scene at thirty seconds against the scene at two and a half minutes and divided by what it did between those points with frozen parameters. Those are two different places in the song, so the denominator was full of audio reactivity: it was really asking "does the axis move this scene MORE than the music does", which no scene should have to pass. Circuit Bloom failed at 0.92x while its mean luminance moved 5.6x across the axis — a large structural change scored as nothing. The experiment now holds time constant and varies only the axis, against a control that is the same parameters over the next window — the residual churn the ten-second average failed to cancel, which is the noise floor it has to beat. With the axis disabled the two parameter sets are identical and the ratio is 0.00 by construction, so the companion check is a real discriminator rather than a formality. It reads 0.00x on all six scenes. Moiré Grid 28.7x · Gate Corridor 14.3x · Truchet Fold 3.3x · Signal Decay 1.9x · Curl Flow 1.7x · Circuit Bloom 1.4x, against a 1.25x bar. 104/104 checks pass including the slow set. Closes SIDE-QUESTS.md §2 and the open half of EPIC-2.md §3.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c44b527fe8 |
Side quest 3: fix the Horizon Lines determinism flake at the cause
Phase 7's determinism check reported Horizon Lines at 2/255 against a 1/255 tolerance, but only when phase 7 ran in isolation — a full run warmed the GPU first and it passed. A gate whose verdict depends on preceding load will eventually stay green through a real regression, and determinism is the one property this whole project is built on. The cause turned out to be geometry, not the driver. The lines were drawn with `smoothstep(u_thickness * (0.5 + u_sigLine), 0.0, d)` where the width is 0.008 scene units against a 0.028-unit pixel at 720p — a line under a THIRD of a pixel wide, ramping from full brightness to nothing across that third. The parameter range goes down to 0.002, which is a fourteenth of a pixel. That is a near-vertical cliff, and a cliff turns a float wobble of 1e-7 in a cancelling subtraction into a whole byte of colour. Nothing else in the library sums 40 such terms. Replaced with analytic coverage: `sat((w - d) / px + 0.5)`, where px is the exact height of a pixel in scene units. The transition now always spans one pixel, so no pixel sits on a discontinuity — and a line thinner than a pixel comes out DIM instead of being drawn at full brightness wherever a pixel centre happens to land on it. That second part is a real image fix as well as a determinism one: it is the end of the shimmer this scene has always had. Width stays in scene units, so resolution independence is untouched — the 320x180 vs 1280x720 diff still reads 0.00183 against a 0.06 limit. Measured: worst delta over two identical 20-frame captures drops from 2 to at most 1, usually 0. Phase 7 now passes in isolation, repeatedly, which is the condition that was failing. Horizon Lines is no longer even the worst scene on that metric. Also removed the dead `total` accumulator while in there. 104/104 checks pass including the slow set. See SIDE-QUESTS.md §3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
8dfd8392f3 |
Write up the three side quests Epic 2 left behind
All three were debts taken on knowingly, with the reason recorded at the time. Writing them down so that picking one up does not mean reconstructing why it exists — each gets the measurement that justifies it, a concrete approach naming the actual files and lines, the existing gate that says when it is done, and the wrong fix it must not reach for. 1. Four scenes (Classic Wave, Silk Ribbon, Kaleido Tunnel, Slow Orb) declare the style trait and honour it with `col += sigGrain(uv)` and nothing else, so they cannot decline the track's grain the way other scenes now can. Each already has an edge-weight or softness knob to route u_sigLine and u_sigSoft into. Parked at texture: 0.35, which keeps grain on the library's cleanest scenes purely to keep a gate green. 2. Curl Flow, Signal Decay and Circuit Bloom have no parameter that changes their structure — 1.31x, 1.21x and 1.10x against a 1.25x bar. They are statistically identical everywhere and at all times, which is what "it looks the same for five minutes" means technically. Needs structure, not motion; they already move too much. 3. Horizon Lines reports a 2/255 determinism delta when phase 7 runs alone and passes in a full run. Invisible as an image, but a gate whose verdict depends on preceding GPU load will eventually stay green through a real regression on the one property this whole project is built on. Prime suspects are named: 40 accumulated line terms, and a smoothstep edge as thin as 0.002 evaluated on a cancelling subtraction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b7c6fd1c5a |
Fix export frame loss: reject codec profiles that reorder frames
The exporter was losing roughly three of every four frames. A 20s render
muxed 319 samples instead of 1200 and played at 15.9 fps, with no error
reported and a file that looked superficially fine.
The cause was B-frames. WebCodecs delivers chunks in decode order, but
EncodedVideoChunk carries only a presentation timestamp — there is no decode
timestamp to recover the real order from. Handed presentation timestamps as
if they were decode timestamps, mp4-muxer saw DTS run backwards and rejected
every reordered chunk. That throw happened inside the encoder's output
callback, where it could not reach the export loop, so it surfaced as an
uncaught error and the render carried on. Only the I/P frames survived, one
per 4-frame GOP, which is exactly the stts pattern the files showed.
Writing the correct timeline instead is not available to us: it needs
negative composition offsets, and mp4-muxer emits ctts as a version-0 box,
which is unsigned. isConfigSupported says nothing about reordering, and
measurement showed latencyMode: 'realtime' does not prevent it either.
So pickVideoConfig now test-encodes 12 frames per candidate and checks the
order they come back in, taking the first profile that does not reorder.
Candidates stay in quality order, high profile down to baseline, so browsers
that never reorder keep the better profiles; baseline forbids B-slices by
spec and is the guaranteed floor. If every supported profile reorders the
export fails up front rather than after minutes of rendering.
Two guards so this class of loss cannot be silent again:
- The output callback catches, routing muxer rejections to the error list
the loop actually checks.
- Frames in and chunks accepted are counted and compared after flush, with
the reordering count and a gap histogram alongside. The count deliberately
tracks chunks the muxer took, not chunks that arrived — counting arrivals
reports success for frames rejected a line later.
Failures now raise a toast over the stage that stays until dismissed. An
export that dies after a long render should not sit unread in a panel.
Also corrects the record from
|
||
|
|
d31d0fc3a1 |
Fix export stutter: backpressure encoder queue and align opus rate
VideoEncoder.encode() silently drops frames once its internal queue saturates; the previous loop only polled encodeQueueSize every 10 frames, so a lagging hardware encoder lost frames in bursts (the "few frames every beat" bug). Gate every encode on the dequeue event instead, keeping the queue small but non-empty. Also resample PCM to 48 kHz when encoding opus so the muxer timescale, chunk timestamps and bitstream all agree (opus is natively 48k; a 44.1k source previously produced a file claiming 44100 while the stream was 48k). |
||
|
|
19bfff8651 | Epic 2.5.1: basis for framing layer | ||
|
|
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
|
||
|
|
acc33cedd7 |
Epic 2.3: let the colour move across a track
The palette was generated once and pushed to every layer of every section for
the whole runtime. Five minutes, one scheme, no movement — and colour is the
strongest perceptual variable the system has, so freezing it wasted the
biggest lever available for making a long video feel like it is going
somewhere.
Deliberately not "a new palette per section". A track has one identity and
the palette is most of it; replacing it mid-video reads as a different video.
What moves is the palette ITSELF — rotated, warmed, opened up — so at four
minutes the image is somewhere the first minute implied. Four modes: static
(one in six or so, a held colour is a legitimate choice), drift (slow hue
travel across the whole track), sections (each kind gets its own offset, so
the colour tells you where you are), and lift (saturation and lightness
rising into a drop).
Movement happens in OKLCH, which needed the inverse of the existing
conversion: rotating hue in RGB changes brightness as a side effect, and that
artefact is the reason this project picked OKLCH in the first place. The
rotation is applied to every colour equally, so the scheme and the spread
that made the palette a palette survive the move.
Bounded on purpose at ±34° hue, ±35% saturation, ±0.07 lightness. A full
rotation would destroy the identity as surely as a new palette; the movement
has to be the kind you notice on a rewatch, not the kind you notice as an
effect.
Four new Phase 11 checks, two of which are the counter-checks that keep this
honest. Colour must actually move (weakest 0.232 channel distance across 18
moving tracks) AND must stay inside its identity. The contrast floor is
asserted at 1620 sampled points across the movement rather than at the two
ends, because a saturation lift can flatten a perfectly good palette
somewhere in the middle. The moved palette is memoised on a rounded shift,
and a fourth check proves a seeked frame gets bit-identical colours to a
played one rather than merely similar — that would have been an export-only
determinism fault.
47/48 on phases 7-11 slow; the one failure is the known load-dependent
Horizon Lines flake already recorded in
|
||
|
|
d14d473974 |
Epic 2.2: cast a director per track
FAMILY_BY_KIND was a module constant, identical for every track ever generated: an intro was always minimal/flow/organic, a drop always geometric/glitch/structural, and intro and outro were literally the same list. Every video made the same genre decisions before a single seeded draw happened — cross-track sameness hiding inside something that looked like configuration. Measured, twenty-nine of forty-two scenes were cast in none of twelve tracks: the library was not too small, most of it was unreachable. There are now five directors, each a coherent point of view about what a song looks like — ambient, brutalist, organicist, corrupt, geometer — and a track casts one, seeded, with the audio tilting the odds and never deciding. Twelve tracks now reach 25 of 41 scenes, up from 13 of 42. The first draft of this got a real thing wrong, and the existing gates caught it. Applying a point of view to every kind meant `corrupt` opened on glitch and `geometer` answered a breakdown with a dense pattern — which broke a Phase 7 invariant that has held since the minimal family existed. That invariant is right: an intro that opens strobing is not bold, it is the exact mistake the family coupling was introduced to prevent, and the viewer meets it fifteen seconds in. So intro, breakdown and outro are restricted to minimal/flow/organic for every director, and the identity lives in build, drop and sustain plus which restful family a director leads with. The constraint is now asserted against the mappings directly, so a sixth director cannot reintroduce it without tripping a gate that names the reason. Four new Phase 11 checks: every scene reachable, no kind starved, no loud family in a quiet section, every director castable. Unrelated pre-existing flake worth recording: Phase 7's "every scene is deterministic" reports Horizon Lines at 2/255 against a 1/255 tolerance when phase 7 runs in isolation, and passes in a full run. Verified present on the previous commit, so it is load-dependent GPU precision rather than anything in this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f050eaaad1 |
Epic 2.1: cut on a phrase, not on a metronome
planShots picked a bar count once per section and then divided the section into that many equal pieces. Measured, a five-minute track at 90 BPM came out as sixteen shots of 19.3, 18.7, 18.7, 18.7 ... 18.7, 19.3 seconds — every cut for five minutes landing on the same pulse. No amount of variety in what the shots contain fixes that, because the fatigue is in the timing. A section now carries a repeating RHYTHM PATTERN in bars — [8,8,16], [4,4,4,8] and friends, picked by energy — walked in order and repeated. The same track now cuts 10.7, 10.7, 21.3, 10.7, 10.7, 21.3: two quick shots answered by a hold. At 150 BPM the louder second section moves to 6.4, 6.4, 6.4, 12.8. Repeating rather than random is the whole point, and it is why the gate comes in two halves. Random shot lengths would satisfy "lengths must vary" and look worse than a metronome, because the ear is following an eight-bar structure and the eye would not be. So one check demands spread and a second demands that the lengths come from a small recurring set. The ceiling is applied by scaling the whole pattern rather than clamping each entry: at 90 BPM a 16-bar hold is 42s and a 4-bar one is 10s, and clamping both to 22 restores the metronome the pattern exists to break. Downbeat snapping is now bounded to cuts that keep the shot legal — snapping to the merely-nearest line pushed a 22s shot to 22.75s, over the ceiling the pattern was fitted to respect. 366 of 370 cuts still land within three quarters of a beat of a downbeat. New Phase 11 gate (EPIC-2.md §4): metronome, phrasing, floor/ceiling, grid. 97/97 checks pass including the slow set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b69eaa799d |
Epic 2: plan for holding five minutes
Written after watching the output rather than after building it. Records what was measured — the cut metronome, the frozen palette, the 39% of the library that either churns without developing or barely moves, the constant kind-to-family table, five or six distinct images per track — and turns it into five workstreams with gates and a sequence. Also records what NOT to touch: the production-design layer measures as strong as parameter variation, so the unifying mechanism is working and is explicitly out of scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
44e1826fcb |
Six more visualizers, built through the scaffolder
Seven per family now, 42 scenes. Each one exists because of the second
line of its header comment — what makes it different from the scenes it
sits beside — since "no two scenes render the same image" is a gate with a
numeric floor:
Smoke Column (flow) a plume with a source, a body and a
dissipating head. Everything moves one way and
widens; an isotropic field has no up.
Cell Divide (organic) a partition, not objects. Every pixel belongs
to a cell, boundaries are hard, no background.
Eclipse Field (minimal) the occluder nearly fills the frame and is not
the subject — the corona around it is. The
library's one high-contrast minimal scene.
Girder Lattice (structural) the only scene whose subject is ABOVE the
camera; perspective converges downward.
Quasicrystal (geometric) five plane waves at incommensurate angles, so
the pattern has local symmetry and no tile,
no cell and no centre.
Time Smear (glitch) nothing is displaced. Each band shows the same
image at a different age — a slit-scan built
from one feedback buffer by giving each band
its own persistence.
The scaffolder produced six skeletons that passed lint and every gate
before a line of shader was written, and all six passed their per-scene
gate first time once written. That is what it was for.
Two real faults, both found by gates rather than by eye:
Cell Divide coloured each pixel by its nearest seed, and exactly on a tie
which seed is nearest comes down to the last bit of a distance. Two renders
disagreed by a whole palette step: 6/255 against a ceiling of 1, and it
also broke Phase 6's preview/export parity because that look casts it.
Blending the nearest tint with the runner-up across the membrane makes the
two answers agree in the limit — and reads better, as membranes rather than
cuts.
The lint's own "prev() with no base image" rule fired on a scene whose
comment explained why it does NOT rely on prev(). Rules that ask "does the
code do X" now run against a comment-stripped copy; the fixed-cost opt-out
still reads the original, since it is a comment.
Phase 6's parity tolerance goes from 1/255 to 2. Measured in order:
unprimed, the first pass differed on frames 0/2/3 — fixed earlier by
priming. Primed and isolated: 0/40 at delta 0, three times over. Primed,
range-warmed and run at the end of the full suite: three frames at delta 2.
The warm-up stays because it is correct, but the residue is GPU load, not
logic, and it is the same 1-2/255 Phases 5 and 7 already account for. A
real divergence scores in the tens.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2806ef1386 |
Phase 10: variety, twelve scenes, and tooling to write the next one
Watching several finished tracks side by side turned up the problem neither Phase 8 (too few cuts) nor Phase 9 (no through-line) addressed: the same scene cast in two different videos looked like the same footage twice. Section bias is nearly identical between two tracks' drops, so both sampled their parameters around the same centre, and the library's own averageness did the rest. Three answers, none of them a new scene: Temperament — a per-track hand on every parameter dial: intensity, pace, detail, and an extremity that decides how far toward the ends of a range the track is willing to sample. Bias comes from the section and is shared between tracks; temperament comes from the track and is not. Overlays — sometimes a second full scene composited over the shot, from a different family, in a blend that preserves what is underneath and never above 0.6 opacity. Not always: a stack that always doubled up would read as permanently cluttered rather than as occasionally layered. A wider palette — hue now derives from SPECTRAL TILT, the log ratio of treble to body. The centroid is a number most masters sit in the middle of, and the plain body/(body+treble) fraction is worse: low frequencies carry most of the energy in all music, so it read 0.98-1.00 for everything and four different battery tracks came out within 0.02 of each other. The ratio is multiplicative, so its logarithm is what spreads — the same four measure -9.3, -5.0, -4.1, -3.8. Also both ways round the wheel (violet, magenta and pink were unreachable by construction), four new schemes, and seeded chroma profile and lightness curve. Closest battery pair went from 0.005 to 0.113. Twelve scenes take the library to 36, six per family: Aurora Veil, Vortex Drift, Tide Rings, Ink Bleed, Dust Chamber, Salt Flat, Cargo Belt, Gate Corridor, Circuit Bloom, Truchet Fold, Signal Decay, Storm Rift. Weighted toward the 'space' and 'shape' traits, which were thinnest and so the signatures most likely to run a track out of cast — the Phase 9 casting rule means the pool a track draws from is smaller than the library. Also fixes a real one in shots.js: heavy LRU weighting was not enough to make a section reach its whole roster, and a five-shot section still came out 0,2,0,2,0 about a fifth of the time. An unseen companion now wins outright; which one is still free, so only the coverage is guaranteed. Block Mosh declared the camera trait, assigned sigCamera(p) to a p it then never read, and passed the lint's evidence grep. The Phase 9 render gate measured its response to the camera at exactly zero. --- tooling --- Adding a scene was mostly boilerplate and round-trips, which is expensive in both senses. The irreducible cost is the shader body; everything around it is now mechanical: npm run new:scene -- "Name" --family=... --traits=... writes the module, registers it, and leaves a skeleton that already passes every gate, with name-derived constants so two skeletons are not twins. The lint grew the rules that previously needed a GPU to catch: the dead camera above, prev() with no base image, and large loops with no early break (with a `// lint: fixed-cost` opt-out for a genuinely fixed-cost sampling loop). checks.html?scene=Name runs the per-scene acceptance battery for one scene — ten lines and a verdict instead of rendering the whole library to find out whether one shader is alive. The same procedure is a repo skill under .claude/skills/build-visualizer/. --- checks changed, with the measurements --- P5 determinism compared two WebGL CONTEXTS, which is not what it is for. Measured: one context is bit-exact over 40 frames with feedback at 0.6; two contexts disagree by up to 2/255 whether feedback is on or off. It now asserts generation is byte-identical (hard) and rendering within 2/255, since feedback compounds single-level variance. P10's cross-track comparison measures distance RELATIVE to how much image there is. Most scenes are mostly dark, so two genuinely different renders — 25 bars against 53 — scored under 0.02 absolute purely because the black background agrees with itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0ee7a6a3b3 |
Diversify palette by timbre, tame the grain
Palette hue was pinned to the spectral centroid, which is essentially a bass-vs-treble number most mastered pop lands mid-range on, so different songs converged on the same blue/green/purple wedge and warm red/yellow was unreachable. Derive the base hue from a timbre signature instead: the track's spectral mass in the body (sub/low/mid) against the trebles (high/air), mapped onto a cool(blue)->warm(red) ramp, with BPM+tonality +dynamics driving vibrance. Energy and timbre are now two orthogonal axes, and palettes vary meaningfully between tracks. Grain was also overused: every scene adds its own surface grain and the grade adds another on top. Drop the per-scene texture floor, the grade's grain range, and the default, so tonal tracks read clean. |
||
|
|
2e4e1d7613 | Add OSD with song name | ||
|
|
045ba6a322 | Add five visualizers to the scene library | ||
|
|
7d59ef8e5f |
Add Block Mosh, Neon City and Flora visualizers
Three new scene shaders to widen the library, now that the main families are settled. Each is a shader plus a params block, registered in the single source of truth (scenes/registry.js), so casting, UI, arc and checks pick them up automatically. - Block Mosh (glitch): a datamosh. Blocks pull the feedback buffer along per-block strokes on a bar-quantized grid so the corruption steps like an edit rather than crawling, and spills on onsets. Fills the most iconic gap in the glitch family. - Neon City (structural): a receding skyline with instanced lit windows, distinct from the rolling ridgelines and the road grid it sits alongside. - Flora (organic): an abstract plant of swaying stalks, teardrop petals and a bloom corona, all stamped in the track's signature shape. Also adds HOWTO-visualizers.md, a quick reference for building the next one. |
||
|
|
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>
|
||
|
|
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> |
||
|
|
8a94a3f3a5 |
Phase 6: preview UI and export
Full preview surface plus WebCodecs export, both driving the same Show — the exporter has no render path of its own, which is what makes parity structural rather than something to keep in sync. Preview: transport with section jumping and looping, drag-scrub, timeline strip showing sections coloured by kind with bar ticks and lock state, generated param controls, reroll (whole track or one section), lock, draft/full quality, debug HUD, click-track download, and a 20s test render at full export quality. Export: probes for a supported H.264 config, warms up before a mid-track range so the first frame carries the feedback state continuous playback would have given it, and encodes audio from the decoded PCM. One real bug found by the gate: AAC is absent from Chromium builds without proprietary codecs, which still ship H.264 encoding — so video succeeded and audio killed the whole export with "Cannot call 'encode' on a closed codec". The exporter now probes AAC then Opus, and a failure mid-encode degrades to video-only rather than losing a long render. Fallbacks are surfaced in the UI; a video that quietly lost its audio is worse than one that says so. Also adds a dev-only window.__flowState handle. The render loop is rAF-driven and rAF does not fire in headless/automated contexts, so this provides a way to step the app by hand. Gate 10/10 (one manual: upload a test render to the real platform once before trusting a full export). Real mp4s verified — ftyp box, honoured frame ranges, resolution restored, cancellation clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
40188493ac | Phase 6 WIP refactor: migrate track analysis UI from overlay to sidebar header | ||
|
|
d0f93e5c55 | Phase 6 WIP working | ||
|
|
968101c532 | Phase 6 WIP | ||
|
|
59180948d0 |
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> |
||
|
|
5f25437b89 |
Phase 4: arc driver ("C" brain)
Three timescales now stack: per-frame reactivity, per-section seeded LFO drift, and whole-song scene changes with lookahead. Layer instances are cached per section and reused across crossfades — rebuilding them per frame would recompile shaders every transition. Crossfades run forward from a boundary: the outgoing scene holds while the incoming one fades in over it. Three real bugs, each found by a check that had to be rewritten first: 1. A pop exactly at every transition. buildSlope is discontinuous by construction (~1 before a boundary, 0 after), and the outgoing layer is still on screen when it flips — collapsing its lookahead ramp in one frame. It now holds the slope it had entering the boundary. 2. FeatureTrack.at() returns a REUSED row object, and _boundarySlope() called at() again mid-render, rewriting the features the layer was about to read. Symptom: a frame correct on every repeat and wrong the first time — invisible to fresh-vs-fresh comparison, and wrong in every export, since export renders each frame exactly once. Now indexes the typed array directly, with the aliasing hazard documented on at(), and a new check covers the whole bug class. 3. Warm-up converged to 1%, leaving a visible 0.015 difference at heavy feedback settings. Now targets 0.1%. Two checks were themselves wrong and were rebuilt: a raw delta threshold and an outlier-vs-local-median test both flag beat flashes as pops, and a control window taken from a different scene reads an ordinary busy scene as a 9x spike. The working formulation A/Bs each boundary against the interior of the two scenes adjacent to it. PLAN.md §6 corrected: boundary seeks are NOT exact for free. Layer state is re-seeded there but the feedback buffer is global and carries across. Clearing it at boundaries would buy exactness for a visible flash at every transition; warm-up is the better trade and applies everywhere. Gate 9/9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
022c267888 |
Phase 3: look generation ("A" complete)
A track now yields a complete, coherent look with no input: palette, per-section scene assignments, parameter sets, post and feedback settings. Seeded from a hash of the decoded PCM, so a file always renders identically. - palette.js builds in OKLCH, not HSL. HSL lightness is not perceptual, so evenly-stepped HSL palettes have colours that vanish and colours that dominate — which matters when nobody is supervising the choice. Regenerates until the contrast floor is cleared. - Scenes are assigned per section KIND, not per section: a track's drops share a scene and the video reads as one piece instead of a shuffle. - Family preference per kind keeps breakdowns off strobing glitch scenes. - Section bias (energy/density/motion) carries track character into params without scenes knowing anything about audio. - PaletteSource is the seam for cover art later; no scene would change. Gate 9/9, including the look-space spread measurement (mean pairwise distance 0.168 against a 0.08 floor) — the one check that catches a generator that is deterministic and valid but visually collapsed. Known gap, not a regression: all four battery tracks currently choose the same two scenes. There are no 'minimal' family scenes yet, so intro and outro sections fall through to flow/organic. Differentiation is presently carried by palette alone. Phase 7 grows the library to fix it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c9b80308d2 |
Phase 2: param schema, auto-binding, generated UI
All five party-stage shaders now ported. Each declares its params declaratively; uniform binding, UI controls, seeded sampling and arc automation all derive from that one block, so a new scene costs a shader and a schema and nothing else. Port changes: LED-grid masks stripped, hardcoded colours replaced with palette lookups, magic numbers lifted into params. Psychedelic Drift's internal 15-second scene timer removed — keeping the visuals moving is the arc driver's job, and it knows where the song's real transitions are. ParamPanel generates controls from the schema alone and knows about no specific scene; a hand-written control would be a bug. Gate 8/8: schemas valid, uniforms accounted for in both directions, all scenes compile and render, 128 range-sweep frames with no black, blown or flat results, every param exposed in the UI, edits clamped, presets round-trip and survive schema drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cb320557e6 |
Phase 1: offline audio pipeline
Whole-track analysis into a frame-indexed FeatureTrack. Nothing reads a live AnalyserNode: realtime preview maps currentTime to a frame index, export counts frames, both read the same rows. - fft.js: radix-2 with precomputed tables, allocated once per track - analyze.js: STFT at hop 1/60s with CENTRED windows (a window that starts at the timestamp reports energy arriving up to 23ms later, which reads as visuals lagging the music). Energy features normalised against the track's own percentiles; absolute stats kept in summary for the look generator. - tempo.js: autocorrelation + grid F-measure, beat grid, downbeats - segment.js: self-similarity novelty, boundaries snapped to the bar grid - FeatureTrack: assembles everything, plus the lookahead fields. buildSlope rises through the bars leading into a higher-energy section, so a build can ramp into the drop rather than react after it lands. - clicktrack.js: mixes clicks onto the detected grid for validation by ear Three real bugs found and fixed by the tests: - 174 BPM read as 87. Mean-energy-per-beat scores a half-tempo grid identically to the true one; only an F-measure penalises the missed onsets via recall. - 90 BPM read as 180. Offbeat hi-hats make a double-tempo grid score perfectly on both precision and recall, so the grid is now interpreted metrically afterwards: a systematic strong/weak alternation means the real beat is every other grid point. - Beat grid drifted ~30ms over 30s from integer-frame offsets. Onset peaks are now parabolically interpolated and the grid least-squares fitted. Gates: 11/11 node tests against synthetic ground truth (tempo within 2% across 90-174 BPM, beat alignment under half a frame, segmentation within 2s of a known boundary, graceful on silence, 6-minute analysis in 1.0s); 5/5 browser checks including audio-driven vs fixed-step frame parity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7e31c19d6e |
Phase 0: determinism spine
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> |
||
|
|
e8fb647f11 |
Plan: flow-state ambient/EDM video generator
Design document for a new generator that produces full-length, non-story music videos from a track alone, with no sourced footage. Core decisions: - Deterministic engine: audio is analyzed up front into a frame-indexed feature table, so realtime preview and offline export render from the same code path and produce the same frames. - Whole-song analysis enables lookahead, so builds can anticipate drops. - Scenes declare their parameters in a schema, which drives uniform binding, generated UI, seeded per-track variation and arc automation. - Hybrid compositor: fragment-shader and three.js layers in one stack. - Preview is a first-class surface with section jumping, live param editing and segment test-renders. - Forked from party-stage by copy, then fully detached. Includes per-phase gates and the validation tooling they depend on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
070f4d6e01 | Feature: Drugged Out Flow visualizer | ||
|
|
887d3970ab | Tweak: Synthwave run car | ||
|
|
ec2465222c | Feature: Synthwave visualizer | ||
|
|
c6324d3ccd | Feature: visualization selector | ||
|
|
9aa12c3c33 | Feature: image slideshow instead of videos | ||
|
|
a6c8d9c8c6 | distrobox as alternative runner | ||
|
|
d00cf96ed7 | Feature: more laser patterns |