music-video-gen/flow-state/src/scenes/registry.js
Dejvino 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>
2026-08-17 21:17:56 +02:00

196 lines
7.0 KiB
JavaScript

import { validateModule } from '../params/schema.js';
// STAGES — scenes that draw the song's cast rather than their own content.
// See scenes/stage/README.md and EPIC-3.md.
import { procession } from './stage/procession.js';
import { constellation } from './stage/constellation.js';
import { soloist } from './stage/soloist.js';
import { swarm } from './stage/swarm.js';
import { nebula } from './shader/nebula.js';
import { classicWave } from './shader/classic-wave.js';
import { floatingGeometry } from './shader/floating-geometry.js';
import { synthwaveRun } from './shader/synthwave-run.js';
import { psychedelicDrift } from './shader/psychedelic-drift.js';
import { particleField } from './layers3d/particles.js';
import { horizonLines } from './shader/horizon-lines.js';
import { spectrumSculpture } from './shader/spectrum-sculpture.js';
import { slowOrb } from './shader/slow-orb.js';
import { curlFlow } from './shader/curl-flow.js';
import { plasmaBloom } from './shader/plasma-bloom.js';
import { metaballs } from './shader/metaballs.js';
import { kaleidoTunnel } from './shader/kaleido-tunnel.js';
import { moireGrid } from './shader/moire-grid.js';
import { ridgeTerrain } from './shader/ridge-terrain.js';
import { scanTear } from './shader/scan-tear.js';
import { blockMosh } from './shader/block-mosh.js';
import { neonCity } from './shader/neon-city.js';
import { flora } from './shader/flora.js';
import { fireflyDrift } from './shader/firefly-drift.js';
import { silkRibbon } from './shader/silk-ribbon.js';
import { pylonGrid } from './shader/pylon-grid.js';
import { prismBloom } from './shader/prism-bloom.js';
import { pitchShatter } from './shader/pitch-shatter.js';
import { auroraVeil } from './shader/aurora-veil.js';
import { tideRings } from './shader/tide-rings.js';
import { dustChamber } from './shader/dust-chamber.js';
import { cargoBelt } from './shader/cargo-belt.js';
import { circuitBloom } from './shader/circuit-bloom.js';
import { truchetFold } from './shader/truchet-fold.js';
import { signalDecay } from './shader/signal-decay.js';
import { inkBleed } from './shader/ink-bleed.js';
import { saltFlat } from './shader/salt-flat.js';
import { stormRift } from './shader/storm-rift.js';
import { vortexDrift } from './shader/vortex-drift.js';
import { gateCorridor } from './shader/gate-corridor.js';
import { smokeColumn } from './shader/smoke-column.js';
import { cellDivide } from './shader/cell-divide.js';
import { eclipseField } from './shader/eclipse-field.js';
import { girderLattice } from './shader/girder-lattice.js';
import { quasicrystal } from './shader/quasicrystal.js';
import { timeSmear } from './shader/time-smear.js';
import { rainColumn } from './shader/rain-column.js';
import { magnetLines } from './shader/magnet-lines.js';
import { karmanStreet } from './shader/karman-street.js';
import { turingBloom } from './shader/turing-bloom.js';
import { myceliumWeb } from './shader/mycelium-web.js';
import { scaleMosaic } from './shader/scale-mosaic.js';
import { pendulumTrace } from './shader/pendulum-trace.js';
import { contourMap } from './shader/contour-map.js';
import { shojiGrid } from './shader/shoji-grid.js';
import { balanceStack } from './shader/balance-stack.js';
import { suspensionSpan } from './shader/suspension-span.js';
import { stairwellDescent } from './shader/stairwell-descent.js';
import { aqueductMarch } from './shader/aqueduct-march.js';
import { dataAisle } from './shader/data-aisle.js';
import { voronoiShatter } from './shader/voronoi-shatter.js';
import { apollonianGasket } from './shader/apollonian-gasket.js';
import { isometricBlocks } from './shader/isometric-blocks.js';
import { halftoneMisprint } from './shader/halftone-misprint.js';
import { drosteFeedback } from './shader/droste-feedback.js';
import { analogWow } from './shader/analog-wow.js';
/**
* The scene library. Families exist so the arc driver can choose by section
* character rather than at random — a breakdown never lands on a strobing glitch
* scene, and an intro never opens at full density.
*/
export const FAMILIES = {
flow: { label: 'Flow', energy: [0.0, 0.7] },
organic: { label: 'Organic', energy: [0.0, 0.8] },
minimal: { label: 'Minimal', energy: [0.0, 0.45] },
structural: { label: 'Structural', energy: [0.3, 0.9] },
geometric: { label: 'Geometric', energy: [0.4, 1.0] },
glitch: { label: 'Glitch', energy: [0.6, 1.0] },
};
const MODULES = [
nebula,
classicWave,
floatingGeometry,
synthwaveRun,
psychedelicDrift,
particleField,
// Phase 7 additions. 'minimal' came first: with the family empty, intros and
// breakdowns fell through to flow/organic and every track opened at density.
horizonLines,
spectrumSculpture,
slowOrb,
curlFlow,
plasmaBloom,
metaballs,
kaleidoTunnel,
moireGrid,
ridgeTerrain,
scanTear,
blockMosh,
neonCity,
flora,
fireflyDrift,
silkRibbon,
pylonGrid,
prismBloom,
pitchShatter,
// Ten added to widen the library past the point where a track's rosters
// start repeating: the casting rule shrinks the pool per track, so depth in
// every family is what keeps two videos from drawing the same four scenes.
// Weighted toward the 'space' and 'shape' traits, which were the thinnest
// and therefore the signatures most likely to run out of cast.
auroraVeil,
tideRings,
dustChamber,
cargoBelt,
circuitBloom,
truchetFold,
signalDecay,
inkBleed,
saltFlat,
stormRift,
vortexDrift,
gateCorridor,
smokeColumn,
cellDivide,
eclipseField,
girderLattice,
quasicrystal,
timeSmear,
rainColumn,
magnetLines,
karmanStreet,
turingBloom,
myceliumWeb,
scaleMosaic,
pendulumTrace,
contourMap,
shojiGrid,
balanceStack,
suspensionSpan,
stairwellDescent,
aqueductMarch,
dataAisle,
voronoiShatter,
apollonianGasket,
isometricBlocks,
halftoneMisprint,
drosteFeedback,
analogWow,
// Stages. Registered alongside the scenes so every existing gate covers
// them; what makes them different is `consumes`, not where they live.
procession,
constellation,
soloist,
swarm,
];
const errors = [];
for (const m of MODULES) {
const e = validateModule(m);
if (e.length) errors.push(...e);
if (m.family && !FAMILIES[m.family]) errors.push(`${m.name}: unknown family '${m.family}'`);
}
if (errors.length) {
// Fail loudly at import: a malformed scene must never reach the compositor,
// where the symptom would be a black frame with no explanation.
console.error('[registry] invalid scene modules:\n' + errors.join('\n'));
}
export const scenes = MODULES;
export const sceneErrors = errors;
export function sceneByName(name) {
return MODULES.find((m) => m.name === name) || null;
}
export function scenesInFamily(family) {
return MODULES.filter((m) => m.family === family);
}
export function familyNames() {
return Object.keys(FAMILIES);
}
export default scenes;