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>
8.9 KiB
Migrating a scene onto the identity artifacts
The recipe tools/migration-status.js reports progress against. Sixty-one
scenes is too many to convert from taste, so this is written to be followed
mechanically and to produce the same result twice.
Background is EPIC-3.md. The one-line version: a scene draws its own content, a migrated scene draws the song's content — its cast, in its ink, on its lattice — and the measured payoff is additive with everything the scene already did.
node tools/migration-status.js # the queue, and what each scene needs
node tools/migration-status.js --all # every scene and its tier
The rule that decides everything
An artifact is content a scene could not have invented for itself.
If the scene still looks right with the identity switched off, it has used the
artifact as a modifier and the migration has not happened. That is not a style
preference — it is the exact failure mode of u_sigSides, which thirty scenes
declare and most quietly ignore, and it is why the gate below exists.
The four tiers
The classifier assigns these from the shader source. Check its answer, don't trust it: it reads patterns, and a scene that places elements in an unusual way will be misfiled.
| tier | what it looks like | consumes |
|---|---|---|
| drawn | loops over discrete elements at computed positions | cast, ink, staging |
| figure | one or a few forms, its own composition | cast, ink |
| field | a continuous surface — noise, flow, terrain | ink |
| treatment | an effect over an image rather than an image | ink |
Accents are excluded. They are mostly-empty depth passes, not subjects.
Recipe: drawn
The highest payoff and the least invention. Four edits.
1. Declare it. Add to the module, above params:
consumes: ['cast', 'ink', 'staging'],
2. Replace the primitive with the cast. Whatever the loop was drawing —
a circle, a box, sigShape, a bespoke SDF — becomes one of:
float d = castMain(q) * size; // the protagonist: large, few
float d = castChorus(q) * size; // the chorus: small, many
q is the element-local coordinate, (p - pos) / size. Multiplying the result
back by size restores it to scene units, which is what the ink expects.
Use castMain when the scene draws a handful of things and castChorus when it
draws a crowd. A scene that draws both should use both — that is what the two
members are for.
3. Replace the placement with the lattice.
vec3 node = stageNode(fi, float(u_count)); // xy position, z scale multiplier
vec2 pos = node.xy;
float size = u_size * node.z;
Keep whatever the scene did that was motion — a drift, an orbit, a wander, a march. Give up what was composition. The split is the point: identity owns where things are, the scene owns what they do.
A scene whose composition IS its identity — a spiral that must be a spiral — can
keep it and take stageScale() alone, which is the song's element size. Declare
staging either way.
4. Replace the edge with the ink.
col = mix(col, pal(i + 1), inkMask(d, uv));
inkMask does fill, fill treatment (hatch, stipple, halftone), outline and edge
hardness in one call. Delete the scene's own smoothstep(soft, -soft, d) and
its sigEdge — the ink supersedes both.
Then wrap the return:
return vec4(inkValue(col), 1.0);
Do not delete sigCamera, sigGrain, sigHorizonY or sigAir. Traits and
artifacts are different layers and both still apply.
Recipe: figure
Steps 1, 2 and 4, skipping the lattice. Declare consumes: ['cast', 'ink'].
Take stageScale() if the figure has a size worth scaling and add staging if
you do.
Recipe: field and treatment
There are no elements to replace, so this is one edit plus a judgement.
consumes: ['ink'],
return vec4(inkValue(col), 1.0);
If the field already dithers, hatches or posterises internally, replace that
with inkPattern(uv) so the treatment is the song's rather than the scene's.
If it does not, inkValue alone is the whole migration — a value structure
shared across every scene in a video is worth having and costs one line.
Be honest about treatment scenes. Most of them are effects wearing a scene's
clothes, and EPIC-3 §8 argues they should move into the identity's EFFECTS
register rather than compete for screen time as subjects. Migrating one is a
holding action, not the answer.
Verifying — the part that makes this reproducible
A migration is not done when the code looks right. It is done when the gate passes:
checks.html?scene=<Scene%20Name>
Every artifact in consumes must show a passing line:
PASS consumes: cast delta 255/255 (floor 24)
PASS consumes: ink delta 255/255 (floor 24)
PASS consumes: staging delta 255/255 (floor 24)
That check renders the scene twice under two deliberately distant identities and
requires the picture to change. A scene that declares cast and ignores it
fails here, which is the only reason consumes can be trusted at library scale.
npm run lint:scenes enforces the other half in both directions: declaring an
artifact without calling it, and calling one without declaring it. The second
matters more than it looks — an undeclared artifact hides the scene from this
report and from anything that later selects scenes on capability.
Then the usual battery still applies. renders something, animates,
deterministic, distinct, param sweep, flash rate and every declared
trait must all still pass. A migration that breaks distinct has made the scene
into one of its neighbours, which is a real risk here: the more scenes share a
cast, the more two weakly-composed ones converge.
Measuring the payoff
Per scene, the gate. Across the library, two numbers:
checks.html?decompose=1 identity against container, measured apart
checks.html?experiment=1 stages against legacy, with error bars
decompose is the one to watch. Before the migration, over the four purpose-
built stages:
identity only 0.0299 container only 0.0557 identity = 54% of container
After the first eighteen scenes, over the eight that consumed the cast:
identity only 0.1287 container only 0.0815 identity = 158% of container
Complete, over all thirty-two:
identity only 0.1054 container only 0.0901 identity = 117% of container
The number that matters in that row is container only, which went 0.0557 →
0.0815 → 0.0901 as the migration progressed. That is the stop condition below
holding: the library did not homogenise. identity only is noisier than it
looks because the scene it probes is picked from the migrated set and changes
between runs — Constellation, then Metaballs, then Floating Geometry — so read
it as "comparable to the container" rather than as a precise ratio.
End to end, song variety across twelve songs before and after the epic:
floor observed separation
before 0.1449 0.1508 not computable (the reference sat below the floor)
after 0.1185 0.1376 0.38
The floor fell and the gap widened, which is both halves of what the inversion was supposed to buy. Every structural block moved with it — scale 40→59%, orient 65→89%, layout 60→111%, texture 89→81%, motion 53→65% of reference.
Coupling did not move: -0.09, still indistinguishable from zero. Whether a song LOOKS different in proportion to how it SOUNDS different remains unsolved, and nothing in the migration addressed it.
Read that with its caveat: the stage set changed underneath the measurement, so
part of the jump is that Metaballs expresses a cast more strongly than
Constellation did rather than that the migration itself moved anything. What it
does establish is the thing worth knowing — a migrated LIBRARY scene carries the
identity better than a stage written from scratch to carry it. The bespoke
stages were the wrong shape of effort. As scenes migrate, the
identity only number should climb while container only holds — because the
whole point of the correction in EPIC-3 §9b is that these add rather than trade.
If container only falls as scenes migrate, the migration is homogenising the
library and should stop.
A caution learned the expensive way
This harness's noise floor is large enough to invent findings. Three claims in
this epic were made from single runs and withdrawn after repeats: that roster
size was the dominant lever, that stages beat legacy by 14%, and that the
identity's range was the bottleneck. Anything under about 0.01 of spread needs
&repeats=3 before it is believed, and a difference that changes sign with the
sample size is not a difference.
Migrate in batches, measure after each batch, and expect the per-batch effect to be inside the noise. The trend across batches is the signal.