// 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;