Files
music-video-gen/flow-state/src/scenes/surface.js
T
Dejvino 46fab512df Venues: persistent room, motivated camera, ride; drop Pylon Grid
- Venue as one continuous place: persistent per-song layer, interpolated
  venueState, story jumps (one re-hang max + recap) with camera cuts
- Room camera shot plan (venueShots.js): per-section size/target, monotonic
  look sweep, traverse glide body, room-side alternation
- New Ride venue: generated vehicle + actor passengers, radio RTT screen,
  chase/stationary modes, ride archetype across generator/camera/staging
- Flora cost trim (5x(12+6+6) -> 4x(8+4+4) SDF evals, 204ms -> 140ms worst)
- Delete Pylon Grid shader (2.5s/frame SwiftShader); drop its metadata row
- tools/venue-check.js headless gate harness + venue-gallery explorer
2026-09-17 19:35:26 +02:00

258 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// What the measurements MEAN — the thresholds and the rules, kept apart from
// the numbers they read.
//
// scenes/metadata.json is generated and holds no opinions: it says a scene
// paints 32% of the frame across six songs and nothing about whether that is
// enough. This file is where that becomes a decision, and it is hand-written
// precisely because a threshold is a judgement about the videos rather than a
// fact about the library.
//
// `surface` used to be declared in each scene file, and it drifted: nine scenes
// said `canvas` while painting under a third of the frame, which is how a
// section ended up standing on a few bright things and black. It is derived
// now. A scene cannot lie about what it paints, because nobody writes it down.
import metadata from './metadata.json';
import { readsHistory } from '../params/schema.js';
/**
* The line between a picture and elements on one.
*
* Half the frame. Under it a scene is not somewhere a shot can happen — it is
* the shot, or something in it, and what would be behind it is black.
*
* The same number does both jobs on purpose: `canvas` and "may be a ground" are
* one idea, and having a scene that is a canvas but not ground-capable would be
* a distinction with nothing behind it.
*/
export const GROUND_MIN = 0.5;
/**
* The bias a GROUND is sampled with, wherever it appears.
*
* A bed is not sampled like a shot. It keeps a density floor whatever the
* section wants — an intro biased sparse would otherwise draw the ground at
* density zero and be thin again for a new reason — and it sits back through
* energy and motion instead.
*
* Defined here rather than in the generator because the measurement has to use
* it too. Coverage depends enormously on parameters: Metaballs paints 69% of
* the frame sampled as a drop's shot and 27% sampled as an intro's bed, and a
* label taken at the first bias tells you nothing about the second. So the
* number that decides whether a scene may be a ground is measured AS a ground.
* One definition, both places, or the two silently disagree.
*/
export const GROUND_BIAS = { energy: 0.5, density: 0.45, motion: 0.3 };
/**
* The hand a GROUND is drawn with.
*
* `extremity` is how far toward the ends of its own ranges a track pushes every
* parameter, and it is the right dial for a shot: the ends are where a scene is
* most itself. It is the wrong dial for a bed. Half of what decides how much a
* scene paints is a parameter with no `bias` key at all — Metaballs' threshold,
* for one — so extremity is the only thing moving it, and one end of that range
* is an empty frame. Measured: a ground drawn at the track's own temperament
* landed on threshold 1.85 of 2.0 and painted 6% of the frame, while the same
* scene measures 58% drawn moderately.
*
* So a bed is drawn near the middle and the extremes are left to the shot,
* which is also the right instinct visually.
*/
export function groundTemperamentFrom(temperament) {
if (!temperament) return temperament;
return { ...temperament, extremity: temperament.extremity * 0.2, intensity: 0 };
}
/**
* The song's hand, as it applies to a GROUND.
*
* The ink treatment — hatch, stipple, halftone, hollow — is how the song draws.
* `hollow` draws outlines and no fill at all, which is a strong, legible
* identity for the thing the section is ABOUT and a disaster for the thing it
* stands on: a scene that paints 61% of the frame averaged over six identities
* paints 2% in the song that asked for outlines, and the section renders black
* with a ground under it. Measured, holding grounds to their worst identity
* instead left four castable beds in the whole library.
*
* So the world is drawn solid and the hand is kept for the subject. Everything
* else about the identity — the cast, the lattice, the line weight, the
* palette — reaches the ground untouched, so it is still unmistakably this
* song's world.
*/
export function groundPersonalityFrom(personality) {
if (!personality || !personality.identity || !personality.identity.ink) return personality;
const { identity } = personality;
if (identity.ink.fill !== 'hollow') return personality;
return {
...personality,
identity: { ...identity, ink: { ...identity.ink, fill: 'flat' } },
};
}
/** The bias `ground` would be sampled with inside `section`. */
export function groundBiasFrom(bias) {
return {
...bias,
// FLOORS, not reductions, on both of the axes that fill a frame.
//
// The first version of this lowered energy to make the bed sit back,
// which turned out to be precisely how to empty it: scenes bias the
// parameters that decide how much they paint against `energy` as often
// as against `density` — Metaballs sizes its blobs on energy, so an
// intro's ground drew radius 0.14 out of a 0.1–0.6 range and painted 4%
// of the frame. A bed sits back through MOTION, which costs it nothing
// in coverage, and through being behind everything else.
density: Math.max(GROUND_BIAS.density, bias.density),
energy: Math.max(GROUND_BIAS.energy, bias.energy),
motion: Math.max(0, (bias.motion ?? 0.5) - 0.2),
};
}
/**
* What a scene is, compositionally.
*
* canvas fills the frame. Belongs underneath, and two of them stacked is
* two pictures fighting rather than one picture with depth.
* composable mostly empty by design. Reads as elements ON something, and
* shown alone it is a few bright things on black — which scores
* well for variety and is thin to watch.
*/
export const SURFACES = ['canvas', 'composable'];
const ROW = (module) => (metadata.scenes && metadata.scenes[module.name]) || null;
/**
* Coverage for a scene nobody has measured.
*
* Two ways to land here: a `layer3d`, which the measuring pass does not render,
* and a scene added since the last refresh. Deliberately pessimistic — an
* unmeasured scene must not be able to talk its way into the ground slot, which
* `canGround` enforces by requiring a real row rather than by trusting this.
*/
const ASSUMED_COVERAGE = 0.15;
export function isVenue(module) {
return !!(module && module.venue);
}
export function venueCoverage(module) {
// Venues are room/stage planes that fill the frame by construction.
// Hard-coded so they pass GROUND_MIN without a gallery measurement.
if (!isVenue(module)) return coverageOf(module);
return 0.72;
}
export function coverageOf(module) {
if (isVenue(module)) return 0.72;
const row = ROW(module);
return row ? row.coverage : ASSUMED_COVERAGE;
}
/**
* How much this scene paints when it is used as a BED — measured at
* GROUND_BIAS, which is the only figure that answers the question `canGround`
* asks. See GROUND_BIAS for why the two numbers differ so much.
*/
export function groundCoverageOf(module) {
if (isVenue(module)) return 0.72;
const row = ROW(module);
if (!row) return ASSUMED_COVERAGE;
if (row.bedCoverageMean !== undefined) return row.bedCoverageMean;
return row.bedCoverage === undefined ? row.coverage : row.bedCoverage;
}
/**
* What this scene paints as a bed in the IDENTITY THAT SUITS IT LEAST.
*
* A song's identity can gut a scene's fill — `hollow` ink draws outlines and no
* fill, a dark palette drops it under the threshold — and the spread is wide:
* Metaballs paints 61% averaged over six identities and 14% in the worst of
* them, which is a black frame with a ground under it.
*
* Held as a SEPARATE, lower bar rather than as the eligibility number. Demanding
* the worst case clear 50% leaves four castable grounds in the whole library —
* every video in the world standing on one of four beds is a worse failure than
* an occasional dim intro. So: fills the frame on average, and never vanishes.
*/
export function groundFloorCoverageOf(module) {
if (isVenue(module)) return 0.62;
const row = ROW(module);
if (!row) return ASSUMED_COVERAGE;
return row.bedCoverage === undefined ? groundCoverageOf(module) : row.bedCoverage;
}
/** A ground may dim under a hostile identity. It may not disappear. */
export const GROUND_FLOOR_MIN = 0.25;
export function isMeasured(module) {
return ROW(module) !== null;
}
/** Derived, never declared. See the header. */
export function surfaceOf(module) {
return coverageOf(module) >= GROUND_MIN ? 'canvas' : 'composable';
}
/**
* Whether a scene can be the ground under a section.
*
* Measured to fill the frame, and self-contained. The second half rules out
* `prev()`: that returns the whole composited previous frame, INCLUDING the
* layers above this one, so a datamosh under a shot is not grounding it, it is
* eating it. Measured when one first became a bed — the render stopped
* reproducing from a seek, and two WebGL contexts diverged by 91/255 against a
* tolerance of 4.
*/
export function canGround(module) {
if (isVenue(module)) return false;
if (module && module.readsDepth) return false;
return isMeasured(module)
&& surfaceOf(module) === 'canvas'
&& groundCoverageOf(module) >= GROUND_MIN
&& groundFloorCoverageOf(module) >= GROUND_FLOOR_MIN
&& !readsHistory(module);
}
/**
* How unalike two scenes are, structurally — 0 is the same picture.
*
* Both profiles are means over the same six songs, so this is a fair comparison
* in a way that two arbitrary renders would not be. Colour is excluded: six
* palettes would otherwise make any two scenes look unalike, and the whole
* point is to find pairs that differ in STRUCTURE.
*
* This is the quantity that makes a stack interesting rather than merely full.
* Two scenes at 0.02 layered over each other are one picture at double density;
* the same two at 0.3 are a picture with something happening in it.
*/
export function structuralDistance(a, b) {
const rowA = ROW(a), rowB = ROW(b);
if (!rowA || !rowB || !rowA.profile || !rowB.profile) return null;
let total = 0, blocks = 0;
for (const block of ['scale', 'orient', 'layout', 'region', 'texture']) {
const x = rowA.profile[block], y = rowB.profile[block];
if (!x || !y || x.length !== y.length) continue;
// Chi-square, matching what variety/signature.js compares descriptors
// with. Duplicated rather than imported because this runs inside the
// generator and the checks must not be a dependency of it.
let d = 0;
for (let i = 0; i < x.length; i++) {
const sum = x[i] + y[i];
if (sum > 1e-9) d += ((x[i] - y[i]) ** 2) / sum;
}
total += Math.min(1, d / 2);
blocks++;
}
return blocks ? total / blocks : null;
}
/** The variety score from the gallery: how much a scene changes between songs. */
export function varietyOf(module) {
const row = ROW(module);
return row ? row.variety : 0;
}
export const METADATA = metadata;