a99a7cbe4906993e2dd41e58f7b57b60cacc5ac6
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a99a7cbe49 |
Continuity rule: discrete params hold per cue; add DESIGN.md + AGENTS.md
Actors and objects change only at camera cuts. int/bool/discrete params skip drift, lookahead and reactivity; discrete slowAxis steps on the section-held journey. Venues fade instead of toggling on interpolated popMul; roadside density is arrangement-only. DESIGN.md collects the project's design decisions; AGENTS.md points agents at it and the build-visualizer loop. |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |