Every shot stands on something, and the frame has two ends

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>
This commit is contained in:
Dejvino
2026-08-20 09:02:01 +02:00
co-authored by Claude Opus 5
parent 7d151be6e1
commit 89e05459c0
68 changed files with 9896 additions and 293 deletions
@@ -19,7 +19,6 @@ export const particleField = {
// top — every layered stack in every song was these particles — while the
// general overlay path sat dead because nothing else was labelled. It is one
// composable scene among many now.
surface: 'composable',
// Points in empty space with no ground of their own — the one scene in the
// library that genuinely cannot carry a section alone. See schema.canBackground.
background: false,
File diff suppressed because it is too large Load Diff
@@ -20,8 +20,6 @@ export const aqueductMarch = {
name: 'Aqueduct March',
family: 'structural',
kind: 'fragment',
// Paints 27% of the frame — see checks/phase12 coverage.
surface: 'composable',
texture: 0.9,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'],
@@ -13,8 +13,6 @@ export const auroraVeil = {
name: 'Aurora Veil',
family: 'flow',
kind: 'fragment',
// Paints 13% of the frame — see checks/phase12 coverage.
surface: 'composable',
consumes: ['ink'],
traits: ['camera', 'space', 'style'],
@@ -13,8 +13,6 @@ export const cargoBelt = {
name: 'Cargo Belt',
family: 'structural',
kind: 'fragment',
// Paints 12% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
texture: 0.4,
consumes: ['cast', 'ink'],
@@ -13,8 +13,6 @@ export const circuitBloom = {
name: 'Circuit Bloom',
family: 'geometric',
kind: 'fragment',
// Paints 1% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
texture: 0.4,
consumes: ['cast', 'ink'],
@@ -8,8 +8,6 @@ export const curlFlow = {
name: 'Curl Flow',
family: 'flow',
kind: 'fragment',
// Paints 21% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
consumes: ['ink'],
traits: ['camera', 'space', 'style'],
@@ -11,8 +11,6 @@ export const dataAisle = {
name: 'Data Aisle',
family: 'structural',
kind: 'fragment',
// Paints 21% of the frame — see checks/phase12 coverage.
surface: 'composable',
texture: 0.6,
consumes: ['ink'],
traits: ['camera', 'space', 'style'],
@@ -21,8 +21,6 @@ export const drosteFeedback = {
name: 'Droste Feedback',
family: 'glitch',
kind: 'fragment',
// Paints 2% of the frame — see checks/phase12 coverage.
surface: 'composable',
texture: 0.5,
consumes: ['ink'],
traits: ['camera', 'style'],
@@ -13,8 +13,6 @@ export const dustChamber = {
name: 'Dust Chamber',
family: 'minimal',
kind: 'fragment',
// Paints 2% of the frame — see checks/phase12 coverage.
surface: 'composable',
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'],
@@ -16,8 +16,6 @@ export const eclipseField = {
name: 'Eclipse Field',
family: 'minimal',
kind: 'fragment',
// Paints 17% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Crisp line work: the track's surface grain would only fur the edges.
texture: 0,
consumes: ['cast', 'ink'],
@@ -9,8 +9,6 @@ export const fireflyDrift = {
name: 'Firefly Drift',
family: 'flow',
kind: 'fragment',
// Paints 2% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
consumes: ['cast', 'ink', 'staging'],
traits: ['camera', 'style'],
-2
View File
@@ -11,8 +11,6 @@ export const flora = {
name: 'Flora',
family: 'organic',
kind: 'fragment',
// Paints 15% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'],
@@ -14,8 +14,6 @@ export const gateCorridor = {
name: 'Gate Corridor',
family: 'structural',
kind: 'fragment',
// Paints 29% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
texture: 0.4,
consumes: ['cast', 'ink'],
@@ -6,8 +6,6 @@ export const horizonLines = {
name: 'Horizon Lines',
family: 'minimal',
kind: 'fragment',
// Paints 16% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
// Crisp line work: the track's surface grain would only fur the edges.
texture: 0,
@@ -13,8 +13,6 @@ export const inkBleed = {
name: 'Ink Bleed',
family: 'organic',
kind: 'fragment',
// Paints 1% of the frame — see checks/phase12 coverage.
surface: 'composable',
consumes: ['ink'],
traits: ['camera', 'space', 'style'],
@@ -12,8 +12,6 @@ export const magnetLines = {
name: 'Magnet Lines',
family: 'flow',
kind: 'fragment',
// Paints 18% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Line work. Grain furs it up, so take only a dusting.
texture: 0.5,
consumes: ['cast', 'ink'],
@@ -7,8 +7,6 @@ export const metaballs = {
name: 'Metaballs',
family: 'organic',
kind: 'fragment',
// Paints 26% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
consumes: ['cast', 'ink', 'staging'],
traits: ['shape', 'camera', 'style'],
@@ -12,8 +12,6 @@ export const myceliumWeb = {
name: 'Mycelium Web',
family: 'organic',
kind: 'fragment',
// Paints 22% of the frame — see checks/phase12 coverage.
surface: 'composable',
consumes: ['ink'],
traits: ['camera', 'space', 'style'],
@@ -11,8 +11,6 @@ export const neonCity = {
name: 'Neon City',
family: 'structural',
kind: 'fragment',
// Paints 26% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
consumes: ['ink'],
traits: ['space', 'camera', 'style'],
@@ -13,8 +13,6 @@ export const pendulumTrace = {
name: 'Pendulum Trace',
family: 'minimal',
kind: 'fragment',
// Paints 0% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Fine line work; grain only furs it up.
texture: 0.25,
consumes: ['ink'],
@@ -7,8 +7,6 @@ export const prismBloom = {
name: 'Prism Bloom',
family: 'geometric',
kind: 'fragment',
// Paints 1% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
// Crisp line work: the track's surface grain would only fur the edges.
texture: 0,
@@ -9,8 +9,6 @@ export const psychedelicDrift = {
name: 'Psychedelic Drift',
family: 'glitch',
kind: 'fragment',
// Paints 6% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'],
@@ -48,8 +48,6 @@ export const pylonGrid = {
name: 'Pylon Grid',
family: 'structural',
kind: 'fragment',
// Paints 25% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
texture: 0.4,
@@ -7,8 +7,6 @@ export const ridgeTerrain = {
name: 'Ridge Terrain',
family: 'structural',
kind: 'fragment',
// Paints 13% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
consumes: ['ink'],
traits: ['space', 'camera', 'style'],
@@ -12,8 +12,6 @@ export const shojiGrid = {
name: 'Shoji Grid',
family: 'minimal',
kind: 'fragment',
// Paints 23% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Paper has a tooth, and this is the one scene that wants it.
texture: 1.0,
consumes: ['cast', 'ink'],
@@ -10,8 +10,6 @@ export const signalDecay = {
name: 'Signal Decay',
family: 'glitch',
kind: 'fragment',
// Paints 5% of the frame — see checks/phase12 coverage.
surface: 'composable',
consumes: ['ink'],
traits: ['camera', 'style'],
@@ -8,8 +8,6 @@ export const silkRibbon = {
name: 'Silk Ribbon',
family: 'minimal',
kind: 'fragment',
// Paints 3% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
// A soft drape of light. Grain furs the one thing it is made of.
texture: 0,
-2
View File
@@ -9,8 +9,6 @@ export const slowOrb = {
name: 'Slow Orb',
family: 'minimal',
kind: 'fragment',
// Paints 11% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
// One soft body in an empty frame — the emptiest scene in the library, and
// the one where speckle is most visible for being least justified. It keeps
@@ -12,8 +12,6 @@ export const smokeColumn = {
name: 'Smoke Column',
family: 'flow',
kind: 'fragment',
// Paints 16% of the frame — see checks/phase12 coverage.
surface: 'composable',
consumes: ['ink'],
traits: ['camera', 'space', 'style'],
@@ -9,8 +9,6 @@ export const spectrumSculpture = {
name: 'Spectrum Sculpture',
family: 'minimal',
kind: 'fragment',
// Paints 2% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
texture: 0.4,
@@ -13,8 +13,6 @@ export const stormRift = {
name: 'Storm Rift',
family: 'glitch',
kind: 'fragment',
// Paints 0% of the frame — see checks/phase12 coverage.
surface: 'composable',
consumes: ['ink'],
traits: ['camera', 'space', 'style'],
@@ -12,8 +12,6 @@ export const suspensionSpan = {
name: 'Suspension Span',
family: 'structural',
kind: 'fragment',
// Paints 21% of the frame — see checks/phase12 coverage.
surface: 'composable',
texture: 0.6,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'],
@@ -10,8 +10,6 @@ export const synthwaveRun = {
name: 'Synthwave Run',
family: 'structural',
kind: 'fragment',
// Paints 5% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
consumes: ['ink'],
traits: ['space', 'camera', 'style'],
@@ -13,8 +13,6 @@ export const vortexDrift = {
name: 'Vortex Drift',
family: 'flow',
kind: 'fragment',
// Paints 21% of the frame — see checks/phase12 coverage.
surface: 'composable',
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'],
@@ -8,8 +8,6 @@ export const constellation = {
name: 'Constellation',
family: 'minimal',
kind: 'fragment',
// Paints 1% of the frame — see checks/phase12 coverage.
surface: 'composable',
consumes: ['cast', 'ink', 'staging'],
texture: 0.6,
traits: ['shape', 'camera', 'space', 'style'],
-1
View File
@@ -15,7 +15,6 @@ export const effigy = {
family: 'geometric',
// One object against a dark wash — most of the frame is legitimately empty,
// and it reads as something standing in a space rather than as the space.
surface: 'composable',
kind: 'fragment',
consumes: ['form', 'ink', 'staging'],
texture: 0.6,
@@ -9,8 +9,6 @@ export const procession = {
name: 'Procession',
family: 'structural',
kind: 'fragment',
// Paints 18% of the frame — see checks/phase12 coverage.
surface: 'composable',
consumes: ['cast', 'ink', 'staging'],
texture: 0.4,
traits: ['shape', 'camera', 'style'],
-2
View File
@@ -8,8 +8,6 @@ export const soloist = {
name: 'Soloist',
family: 'minimal',
kind: 'fragment',
// Paints 20% of the frame — see checks/phase12 coverage.
surface: 'composable',
consumes: ['cast', 'ink', 'staging'],
texture: 0.5,
traits: ['shape', 'camera', 'style'],
-2
View File
@@ -16,8 +16,6 @@ export const swarm = {
name: 'Swarm',
family: 'organic',
kind: 'fragment',
// Paints 2% of the frame — see checks/phase12 coverage.
surface: 'composable',
consumes: ['form', 'ink', 'staging'],
texture: 0.5,
traits: ['shape', 'camera', 'style'],
+241
View File
@@ -0,0 +1,241 @@
// 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 coverageOf(module) {
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) {
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) {
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) {
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;