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>
31 KiB
Epic 4 — the song tells a story
Epic 3 asked what is on screen. This one asks a question none of the existing layers can answer:
Why is this the last drop rather than the first one?
Nothing in the generator knows. And a viewer who cannot tell the difference is watching a loop with good production design.
1. What is actually missing
The generator has four timescales, and ArcDriver's header lists them honestly: per frame,
per shot, per section, whole song. Three of the four are cyclic or local. The fourth — the
"whole song" one — turns out to be much thinner than it reads:
_slowAxisForis the only thing in the system that travels one way across the track. It moves one or two params per scene,journey = smoothstep(t/duration), in a direction picked byrng.bool(). It is real progression, and it is blind: it does not know where the drop is, it does not know a section from a boundary, and half the time it travels the wrong way for what the song is doing.paletteArc.underDrift— a bounded hue crawl, plus per-kind offsets.buildSlope— one bar of lookahead. Local by design.
Everything else that decides what a video looks like is keyed on section kind, and kinds recur:
assignRostersByKind() roster per KIND — every drop cuts between the same visuals
KIND_ENERGY / biasFor() same energy, density, motion for every drop
rhythmFor(energy) same cutting pattern for every section of that energy
derivePaletteArc() kindHue.drop — the same hue offset at every drop
frameShot(style, ...) framing from previous shot + energy; no notion of when
So the fourth drop is cast from the same roster as the first, biased to the same energy,
cut at the same rate, tinted the same hue and framed by the same rule. The content of the
video is a function of kind, and kind has no arrow of time in it. That is precisely a
song structure without a story: recurrence without consequence.
This is not a bug in any of those modules. Kind-keying is what gave the video its identity
(§assignRostersByKind), and it should stay. What is missing is the second coordinate.
Today a section is identified by what kind of thing it is. It should be identified by what kind of thing it is, and where in the story it sits.
2. The proposal, in one paragraph
A new pure module, look/Story.js, runs once per track between segmentation and the look
generator. It reads the section list and the summary, picks a plot the way
directors.js picks a director, locates the song's key moments, and emits a small
narrative state per section and per frame. Everything that currently keys on kind
keys on (kind, story) instead. No scene changes, and — in the first slice — no new
uniforms: the story acts by rewriting inputs the whole library already consumes, so it
applies to all 61 scenes on day one.
3. What a story is, here
Three layers, smallest to largest.
Position — where a section sits
Derived, not invented. From the sections segment.js already produces:
{
index, kind,
ordinal, ordinalOf, // "the 3rd of 4 drops" — the single most useful missing fact
act, // setup | development | turn | climax | resolution
isMoment, // one of the key moments below
}
ordinal alone unlocks most of the devices in §5, and costs a Map and a loop.
Moments — the frames the story turns on
Measured from the track, never imposed. A short list, and each one has a definition that falls out of data already on the FeatureTrack:
| moment | definition |
|---|---|
| arrival | first section whose energy clears 0.6 × max — the video's first "here it is" |
| turn | largest energy fall between adjacent sections after the arrival |
| climax | max-energy section; ties broken toward the later one |
| resolution | first section after the climax whose energy stays below it |
A two-section track collapses these onto each other, and that is fine — a track with no structure gets almost no story, which is the correct behaviour and not a degenerate case to defend against.
Plot — what the track does with them
One coherent narrative shape per track, chosen weighted-random with the audio tilting the
odds, exactly the way pickDirector works, and for the same reason: a fixed mapping from
measured features to narrative is how a library ends up with one story per genre.
| plot | shape |
|---|---|
| emergence | almost nothing, then something. Reveal rises monotonically and stays. |
| escalation | each recurrence of a kind is further than the last. A ratchet, not a curve. |
| collapse | order → entropy. The climax is a breakdown of the thing, not a peak of it. |
| return | ABA. The outro rhymes with the intro, transformed by what happened between. |
| unveiling | the protagonist is withheld until the climax and then it is all there is. |
Each plot is a set of curves over five story variables, evaluated per frame:
tension 0..1 how hard everything is pushed
reveal 0..1 how much of the song's identity has been shown
closeness 0..1 wide and distant → close and involved
population 0..1 sparse → crowded → alone
order 0..1 regular → broken (or the reverse; the plot decides the sign)
These are staged curves, not ramps: they hold flat inside a section and move at boundaries, with a step at the climax. That is what makes them read as a story rather than as a slow zoom — a story advances in scenes.
4. Where it plugs in
Every one of these is an existing call site gaining an argument.
| site | change |
|---|---|
biasFor() |
tension modulates energy/density within the kind's envelope, bounded to ±0.2, so a breakdown at high tension is still a breakdown |
sampleValues(…, temperament) |
extremity scaled by tension — the ratchet for escalation |
assignRostersByKind() |
roster stays per kind; which member plays becomes a story decision (§5.1) |
buildStack() |
overlay chance follows population, not just energy |
rhythmFor() |
later acts pick from faster patterns; the resolution gets a held shot |
frameShot() |
closeness biases the size draw |
derivePaletteArc() |
new 'narrative' mode keyed on act rather than on kind |
ArcDriver._paramsAt() |
journey comes from story.journeyAt(frame); the slow axis gets its sign from the plot, not from rng.bool() |
ArcDriver.update() |
passes a story-shifted personality clone, memoised on rounded reveal exactly as _paletteAt memoises on rounded shift |
That last one is how the story reaches Epic 3's content registers without a new uniform:
setPersonality is already called every frame, and the identity uniforms are derived from
the personality object. Scaling notchDepth, hollow, inkOutline, posterize and
latScaleSpread toward their full values as reveal rises makes the song's cast literally
arrive over the course of the video.
5. The devices, ranked by legibility per unit of work
5.1 The anchor is earned. Today roster[0] opens every section of its kind. Instead,
reserve it: earlier drops play companions, and the anchor arrives at the climax. Same
roster, same identity, and now the biggest visual in the video lands on the biggest moment.
Roughly twenty lines.
5.2 Recapitulation. Under the return plot, the outro re-casts the intro's scene, with the story's parameters rather than the intro's. The oldest device in music video and the cheapest one here — the scene is already built and cached.
5.3 The ratchet. Under escalation, ordinal/ordinalOf scales temperament extremity
and slow-axis travel per recurrence. The fourth drop is measurably further out than the
first, on every param the scenes declare.
5.4 Reveal schedule. §4's identity scaling. Under unveiling the protagonist's
u_cast* form is a near-circle until the climax, then snaps to full at a downbeat.
5.5 Punctuation. A budget of two or three single-use events for the entire video, spent at the moments in §3 — the only cut to black, the only symmetry-fold flip, the only feedback reset. Single-use is the whole point: a device used twice is a style, used once it is a moment. These are the only additions that need a flash-gate review.
6. How to know if it worked
The existing instrument can measure this almost unmodified. Build the section×section
descriptor distance matrix that checks/variety already knows how to produce, and ask
three questions of it:
- Direction. Does distance correlate with |i−j| beyond what kind explains? Today this is ~0 by construction: the matrix is kind-blocked, all drops mutually near, and time is invisible. A story makes it a gradient. This is the headline number.
- Recurrence. For a repeated kind, is
d(first, last) > d(first, middle)? That is the ratchet, and it is the one a viewer names as "it kept going somewhere". - Coherence bound. Adjacent-section distance must stay under the existing ceiling. A story that maximises Direction by shuffling scenes is the failure mode, and this is the gate that catches it.
Plus: each punctuation fires exactly once, on a downbeat, under the flash limit; and every curve is a pure function of frame, so seek-exactness and the determinism grep hold.
A prediction, stated in advance: the seed-variety floor — how far a video travels from itself — will rise, because that is what progression is. Epic 3 spent its effort pushing that number down. Both are correct, and the instrument is what is wrong: it measures distance and calls it drift, with no way to tell wandering from travelling. The fix is one extra statistic, not a retreat from the feature — split the self-distance into an ordered component (monotone with time; a story) and an unordered residual (a shuffle). Ship that statistic before the feature, or the first honest measurement of Epic 4 will read as a regression and be argued about instead of read.
7. Risks
The story overrides the song. A plot that declares a climax where the track is quiet is
worse than no plot. Mitigation is structural: moments are found in the audio (§3), never
placed by the seed, and tension is bounded inside the kind envelope so the quiet-kind
coupling in directors.js — the one that keeps an intro off a strobing scene — still holds
absolutely.
Every video tells the same story. The exact failure directors.js was written to fix.
Same mitigation: five plots, weighted, with seeded curve parameters inside each.
It becomes a slow zoom. If the curves are smooth ramps, this is an effect, not a narrative. Staged curves with plateaus and a step at the climax are load-bearing, not a refinement.
Short tracks. Under three sections, most of this has nothing to work with. Degrade to the current behaviour explicitly rather than letting the curves do something arbitrary.
8. The smallest experiment worth running first
Do not build five plots on a prediction. Three changes, no new uniforms, no scene edits:
Story.jswith position and moments only — no plot templates, one hardcoded escalation curve.- Wire it to exactly two sites: the slow-axis sign/magnitude in
_paramsAt, and the anchor reservation in §5.1. - Add the Direction statistic to the variety report and run the song bank.
The prediction is specific: Direction moves off zero and Coherence holds, while the between-song distance is unchanged — the story should differentiate a video from itself in time, and have no opinion about other songs. If Direction does not move, the story is not reaching the image and the rest of the epic is worth nothing until it does.
11. What was built, and what it measured
Built, in look/Story.js plus one argument at each call site listed in §4:
- position (
ordinal/ordinalOf/act), the four moments, five plots with seeded curves; - the anchor is earned (§5.1), the ratchet on temperament (§5.3), recapitulation (§5.2),
the reveal schedule on the identity uniforms (§5.4), story-driven cut rate, framing
closeness, and a
narrativepalette-arc mode; - the slow axis now takes its direction from the track rather than a per-scene coin flip, and its journey from the staged story curve;
- Phase 13 (
checks/phase13.js, 8 checks, no GPU) and the direction statistic in the variety report (checks/variety/signature.js).
Punctuation (§5.5) was not built — it is the only part that needs a flash-gate review, and it is worth doing after the numbers below are understood rather than before.
The first direction measurement
checks.html?variety=1&library=0&seeds=6, one song, against the arcless single-scene
reference:
floor 0.1697 direction -0.14
observed 0.1594 arcless ref 0.01
ceiling 0.1996
The prediction in §8 was that direction moves off zero upward. It did not. Three readings, in the order they should be checked:
- The recapitulation is fighting the statistic, by construction. Roughly two videos in
five recap, and
return— the plot most likely to — is an arch that comes back. Its first and last probes are deliberately similar, which is exactly what a negative rank correlation between time separation and distance means. The statistic as written cannot tell ABA from no story at all; it may need to be measured against the journey curve rather than against clock time. - Six seeds of one song is a small sample, and the probe count (5) makes each video's correlation rest on ten pairs.
- The arc may not be reaching the image, which is the reading that matters and the one
§8 was written to expose. If 1 and 2 are controlled for and direction stays at zero, the
story is moving parameters that do not change the picture — the same failure the slow
axis had before scenes declared
slowAxis, and the fix would be the same: name the levers rather than guessing at them.
The no-story control arm (the same report with the story layer bypassed) did not complete — the run hung in the browser after the first arm, so the floor and separation figures above are not yet attributable to this work either way. That comparison is the next thing to run, and it should be run before any conclusion is drawn from the numbers.
12. The second direction measurement — reading 3 was right
Same invocation, checks.html?variety=1&library=0&seeds=6, after the two fixes in §12.1:
§11 now
direction -0.14 +0.24 arcless reference -0.02
floor 0.1697 0.2130
§8 predicted direction moves off zero upward and that the floor rises, because that is what progression is. Both happened, and the epic's headline hypothesis is confirmed.
Of the three readings offered in §11, reading 3 was the correct one: the arc was not reaching the image. The story layer was never the problem — it had nothing to speak through. Two of the channels a narrative would have to travel down were inert, and neither failure was visible from the source:
The overlay path was structurally dead. buildStack gated layering on
surfaceOf(m) === 'composable', and no scene in the library declared surface — so the
only scene that could ever sit on top was the one declaring role: 'accent', which the
overlay roster excluded by construction. Empty intersection, every time: 0/144 stacks
carried an overlay. population had been wired to the overlay chance per §4 and moved a
number nothing read. Fixed by labelling the library from the phase 12 coverage gate — the
37 scenes painting under 30% of the frame are composable — and collapsing the reserved
accent slot into one path. 1 distinct overlay scene became 27, at 34% of stacks.
The camera was inert. framing.shift moved the frame by a median of 0.029 of a
half-frame at a fresh uniform angle every shot, so successive offsets cancelled and the
median jump at a cut was 0.014. closeness reached shot SIZE per §4; the recentre got a
per-track constant and a die roll. Fixed by look/Camera.js — see §12.1. Median offset is
now 0.190 and the median reframe 0.135.
This is the same failure mode the slow axis had before scenes declared slowAxis, exactly
as reading 3 predicted, and it had the same shape: a lever that existed, was wired, was
covered by gates, and moved nothing. Both passed every check that existed, because every
bound on them was a ceiling. A device doing nothing clears a ceiling comfortably.
The lesson worth carrying out of this epic: a gate on a device needs a floor, not only a limit. Three of the four new camera checks are floors for this reason.
Readings 1 and 2 are still uncontrolled — the recap arch and the small sample both still apply, and the honest statement is that direction was measured under them both times, so the change is attributable even though neither absolute figure is clean.
12.1 Built since §11
look/Camera.js— the director's camera department, and the answer to "who translates story into imagery". Story says tension, order and act; this turns that into where the frame looks and how it travels there. Each director indirectors.jsnames a camera. Jump distance follows tension and act, speed follows energy, curve is one of four, and a cut chooses between reframing and matching so two scenes can still read as one place. 94% of shots now move during the shot — the recentre is no longer per-shot constant, which is a deliberate departure from the rule inframing.js(that rule was right about size and wrong about where the camera is looking).surfacedeclared across the library,role: 'accent'retired in favour ofsurface: 'composable'plusbackground: false, and one layering path instead of two.- Four camera gates in phase 11, each with a floor.
13. What is left
Ranked. Three of the four are measurements, and they all report through an instrument that is currently broken — hence the ordering.
13.1 The seed-variety instrument does not report. Not in the original plan, and now the first thing to fix:
separation — not computable: the reference landed below the floor
floor 0.2130 · ceiling 0.2179 · all six seeds read "thin"
The ceiling — videos built from casts sharing no scenes — is 0.2179 against a floor of
0.2130. Real seeds already differ by about as much as maximally-unrelated ones, so
separation divides by a near-zero interval and comes back NaN. This is the separation NaN in the standing phase 12 failure, and it has been there across every run including the
pre-work baseline.
§11 anticipated this precisely — "the instrument is what is wrong" — and proposed the direction split as the fix. But direction was added alongside the broken separation rather than replacing it, so the gate now leads with an unreadable number while a working one sits underneath. The decision to make: repair the ceiling, or retire separation and read (floor, direction) as the pair.
13.2 The coherence bound (§6). Never built. Optional while direction sat at zero; load-bearing now that it is +0.24, because nothing currently stops future work from buying direction by making adjacent sections incoherent — the named failure mode in §6.
13.3 Recurrence (§6). Never built. phase13 has a spec-level cousin — was the kind
recast, did tension move more than 0.08 — but not the descriptor-distance statistic
d(first, last) > d(first, middle). That is the ratchet measured in the image rather than
in the spec, and §5.3 calls it the one a viewer names as "it kept going somewhere".
13.4 Punctuation (§5.5). Still the only part of the plan wholly unbuilt. Nothing in
src/ mentions it. Needs the flash-gate review, which is why it was deferred; §11 said to
do it once the numbers were understood, and they now are.
13.5 The no-story control arm. Still never completed. Less urgent than when §11 was written — direction clears the arcless reference by 0.26, which is hard to explain without a story — but it remains the only thing that would attribute the floor rise (0.1697 → 0.2130) to this work rather than to everything else that changed alongside it.
14. Composition — the frame has a floor and a ceiling
Not part of the original plan. It comes out of the same place §12 did: the layer stack was being asked to carry the video, and half of what it was stacking was nothing.
The floor. Two thirds of the library is composable — sparse by design, elements ON
something. canBackground let almost all of it anchor a section anyway, so a section's
bottom layer was regularly a scene painting 2% of the frame with black behind it. Measured
across six videos, sampling the middle of every section: 9 of 40 frames were under 20%
painted, the darkest at 0.3%. That is a minute and a half of a few bright things on black,
and no gate could see it, because every gate on the stack was a limit rather than a floor.
Every section now stands on a GROUND: a canvas painting at least half the frame, cast per section kind so a section's shot cuts change the shot and not the world. When the shot is itself a full canvas it is the ground — two canvases stacked is two pictures fighting. The same measurement now reads mean 92% painted, darkest 63%, nothing under 30%, and phase 12 gates it in the image, not in the spec.
The ceiling. Layering had a rate but no budget, so how full a frame got was a die roll
that knew nothing about the song. A stack now has a coverage budget — director appetite
(crowd, 1.05 brutalist to 1.7 corrupt), times the section's energy and density, times where
the story is — capped at 200%, two frames' worth of material. The ground and the shot are
paid for first; the budget governs what may be stacked on top, and the odds of an overlay
fall off as the headroom does rather than only at the wall.
What it cost, and what it bought. Same instrument, same six seeds:
baseline with grounds
separation 0.10 0.44
direction 0.06 0.21 (arcless reference 0.02 → 0.08)
library 80% 97% of what the library can express
scale 42% 56%
motion 68% 96%
texture 74% 90%
The ceiling fell (0.2334 → 0.2031) while observed rose, so part of that separation gain is the reference arm coming down: videos that share a ground vocabulary are less unalike even when they share no shots. The honest reading is that the floor rose 0.009, direction tripled, and the ceiling moved toward the floor — the numbers are up, and not all of the rise is signal.
A scene that reads the previous frame cannot be a ground. prev() returns the whole
composited frame, including whatever is layered on top of this scene, so a datamosh under a
shot is not grounding it, it is eating it. Measured the moment one became a bed: the render
stopped reproducing from a seek, and two WebGL contexts diverged by 91/255 against a ceiling
of 4 — phase 4 caught both. readsHistory is derived from the shader source rather than
declared, and costs the ground pool six of twenty scenes, five of them glitch.
scenes/coverage.js is the measured coverage of every scene, pasted back out of the phase 12
gate, which re-measures and fails on drift over a tenth of a frame. A new scene has no entry
and therefore cannot ground anything until someone renders it.
14.1 The measurements moved into the repo
Everything in §14 depends on a number — how much of the frame a scene paints — and the
first version of it was a table pasted into a source file by hand. That is the same mistake
surface already was: a measured fact written down by a person, correct on the day and
wrong after the next shader edit.
src/scenes/metadata.json is now generated, tracked in git, and holds every measured
fact about every visualizer: coverage as a shot, coverage as a bed, the variety score, the
per-block scores, and the mean structural profile. Nothing in it is typed by a human.
surface is derived from it (canvas at 50%, composable below) and declaring surface: in a
scene file is now a lint error.
It is refreshed from gallery.html → refresh metadata, which re-renders the library and
writes the file through a dev-only endpoint in vite.config.js. The file carries a
fingerprint of everything that can move a number in it — the scenes, the shader contract,
the identities and palettes they are handed, the descriptor definitions — and phase 12 fails
when that stops matching. A stale metadata file does not produce a stale report, it produces
wrong videos, so it has to be detectable.
Measuring at the wrong bias measures nothing. Coverage is mostly a function of a scene's parameters, and the first measurement took one number per scene at the busiest section of six songs. Metaballs measures 69% there and painted 6% as an intro's ground. Three things came out of chasing that, in order of how much each was worth:
- A bed is drawn with a moderate hand.
extremitypushes parameters toward the ends of their ranges, and half of what decides coverage is a parameter with nobiaskey at all — Metaballs'threshold— so extremity is the only thing moving it and one end is an empty frame. Grounds sample atextremity * 0.2. The extremes belong to the shot. - Energy and density are floors for a ground, not reductions. Calming the bed by
lowering energy is how to empty it: scenes bias their fill against
energyas often as againstdensity. - Coverage is measured twice, once as a shot and once as a bed, at the same bias and
temperament the generator will use.
GROUND_BIASandgroundTemperamentFromlive inscenes/surface.jsand are imported by both the generator and the measuring pass, so the two cannot drift apart.
Rendered result across six videos, sampling the middle of every section: mean 87% painted, darkest 30%, against 51%/0.3% before any of this.
Eleven scenes can ground a section — five organic, five geometric, one structural. There
is no minimal, flow or glitch canvas in the library that paints half the frame without
reading prev(), so every quiet section in every video stands on one of five organic beds.
That is the largest remaining hole and it is library work, not generator work: the fix is
writing dense, self-contained canvases in the three thin families.
14.2 The red line is gone
MIN_VARIETY — 0.1, drawn across the gallery, "below this a scene is the same picture
wherever it appears" — was right when a section was one scene and is wrong now. A section is
a ground, a shot on it and sometimes a pass over that, so what a viewer sees is a
combination, and a scene that is reliably itself is a good ingredient in one. Nineteen
scenes were failing a bar for being consistent.
What replaced it is a level up, and it is the reason the profiles are in the metadata: the
generator weights every layering choice by structural distance — how unalike two scenes
measured, on the same descriptor the variety harness compares videos with. Family labels and
the render disagree often enough to matter; two geometric scenes can be 0.31 apart and a
flow and an organic scene 0.04, and stacking the second pair is one picture at double
density. Measured over 436 stacked pairs: mean distance 0.268, zero near-twins.
14.3 The other end of the frame
The floor had a ceiling missing. Measured across twelve videos, sampling the middle of every section: a median 24% of every frame was clipped to pure white, and whole sections rendered at 100%. The videos were washed out, and no gate could see it because every guard on the frame asked whether there was enough in it.
Three causes, all introduced or exposed by §14, in the order they were found:
The shot was screened over its ground. Screen is a lightening operator — correct for a
few bright elements over a bed, and over a filled canvas it drives everything toward white.
Replaced with a lumakey blend (passes.js): the shot's own brightness is its alpha, so
it replaces the ground where it paints and leaves it where it does not, keeping its own
colour instead of adding it to the bed's. Median clipping 24% → 11%.
The feedback loop was an accumulator. cur + hist * decay * amount settles a still
image at 1/(1 - decay*amount) times its drawn brightness — 2.3x at the settings the
generator hands out. Survivable when a frame was a few bright things on black; fatal the
moment every section stood on a filled ground. Turning feedback off took a blown frame from
100% to 34% mean luminance, which is the whole diagnosis in one number. It now divides by
the same gain, at 0.6 rather than 1.0: full normalisation took the lift out along with the
blowout — the median frame fell from 95% painted to 71% and fourteen sections dropped
through the black-frame floor.
Nothing rolled off. The grade clipped. A highlight shoulder now compresses everything above 0.75 toward but never to 1. Clipped white is not brightness, it is missing information: every difference inside it has been deleted.
Rendered, after all three — 74 sections, twelve videos:
before after
painted 51% mean 88% mean, darkest 43% (was darkest 0.3%)
clipped white 24% median 0% median, worst 21% (was worst 100%)
luminance 77% median 42% median, worst 80%
The gate is now two-ended: composition · a rendered section is neither black nor blown out.
A ground has to survive the song's identity. Chasing the dark end turned up the same
class of error as §14.1: a ground measured at 61% painted 2% in one particular video,
because that song's ink treatment is hollow — outlines, no fill. Two changes came out of
it. The world is now drawn solid and the song's hand is kept for the subject
(groundPersonalityFrom), and eligibility takes the worst identity into account as well
as the mean: a ground fills the frame on average and never vanishes. Holding the worst case
to the full 50% would leave four castable beds in the library, which is a worse video than
an occasionally dim intro.
Seven scenes can ground a section — five geometric, two organic. Every quiet section of every video stands on one of two beds. Restated from §14.1 because it got worse, not better: this is the library's largest hole.
14.4 Blazing on purpose
Screening the shot over its ground and letting the two brightnesses sum is a real effect — a drop that goes to paper for eight bars reads as the song peaking. The mistake in §14.3 was not the effect, it was that the effect was the default: every section did it, so a median quarter of every frame in every video was clipped, and nothing about that said "peak".
It is a decision now. Each director declares a blaze appetite — brutalist 0.05 (mass does
not glow), corrupt 0.5 (overload is the subject) — and a section still has to earn one: loud,
and late in the story. Quiet kinds never blaze, because a breakdown that goes white is not a
choice, it is a bug with a rationale. The default composite is the lumakey, which keeps the
shot's own colour.
The ceiling gate matches the intent rather than banning brightness. Per section, only a hard cap — past about half the frame at full white there is no picture left to read. Across the population, no more than a fifth of sections may be hot at all, which is the number that actually distinguishes a director choosing to peak from a pipeline with no headroom. Measured now: 1 of 74 sections blazing, worst 30% clipped.