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>
67 lines
2.3 KiB
JavaScript
67 lines
2.3 KiB
JavaScript
import { defineConfig } from 'vite';
|
|
import { resolve } from 'path';
|
|
import { writeFileSync } from 'fs';
|
|
|
|
/**
|
|
* Let the measuring pages write their results back into the source tree.
|
|
*
|
|
* The measurements that describe the scene library — how much frame each one
|
|
* paints, how much it changes between songs, what it looks like structurally —
|
|
* can only be taken by rendering, and rendering needs WebGL, which means a
|
|
* browser. So the numbers are produced in a page and have to get from there
|
|
* into a git-tracked file. This is that hole in the wall.
|
|
*
|
|
* Dev only, by construction: `apply: 'serve'` means it does not exist in a
|
|
* build, so a deployed gallery cannot write anything. The path it may write is
|
|
* a fixed allow-list rather than a parameter — a request body decides the
|
|
* CONTENTS of a known file and never which file.
|
|
*/
|
|
function measurementWriter() {
|
|
const WRITABLE = {
|
|
'/__metadata': 'src/scenes/metadata.json',
|
|
};
|
|
|
|
return {
|
|
name: 'flow-state-measurement-writer',
|
|
apply: 'serve',
|
|
configureServer(server) {
|
|
server.middlewares.use((req, res, next) => {
|
|
const target = WRITABLE[req.url];
|
|
if (!target || req.method !== 'POST') return next();
|
|
|
|
let body = '';
|
|
req.on('data', (chunk) => { body += chunk; });
|
|
req.on('end', () => {
|
|
try {
|
|
writeFileSync(resolve(process.cwd(), target), body);
|
|
res.statusCode = 200;
|
|
res.end(`wrote ${target} (${body.length} bytes)`);
|
|
} catch (err) {
|
|
res.statusCode = 500;
|
|
res.end(String(err && err.message));
|
|
}
|
|
});
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
export default defineConfig({
|
|
plugins: [measurementWriter()],
|
|
server: {
|
|
port: 5180,
|
|
host: '127.0.0.1',
|
|
},
|
|
build: {
|
|
rollupOptions: {
|
|
input: {
|
|
main: resolve(process.cwd(), 'index.html'),
|
|
checks: resolve(process.cwd(), 'checks.html'),
|
|
debug: resolve(process.cwd(), 'debug.html'),
|
|
gallery: resolve(process.cwd(), 'gallery.html'),
|
|
filmstrip: resolve(process.cwd(), 'filmstrip.html'),
|
|
},
|
|
},
|
|
},
|
|
});
|