Composable vs canvas: say which scenes are meant to sit on top of others

The top of the gallery is full of black frames with a bright element on them.
That is genuinely varied and thin to watch, and the reason it ranks is that the
gallery previews every scene ALONE — which is not how anyone watches one.

Measured, the split is not marginal: 37 of 66 scenes paint under 30% of the
frame, and the sparsest are exactly the ones topping the ranking. Storm Rift and
Pendulum Trace paint 0%, Droste Feedback and Spectrum Sculpture 2%. At the other
end Block Mosh covers 98% and Scan Tear 96%.

So a scene now declares a `surface`. A canvas fills the frame and belongs
underneath; two of them stacked is two pictures fighting rather than one picture
with depth. A composable scene is mostly empty by design and reads as elements
ON something. The distinction was implicit in `role: 'accent'`, which marked the
depth passes and said nothing about the sixty scenes treated as interchangeable
backgrounds while half of them were sparse.

Verified rather than trusted, in the pattern the rest of the project uses: a
gate renders every scene and measures what it actually paints, failing a
composable that covers more than 55% — it would hide whatever it sits on — and a
canvas that covers less than 8%, which is a canvas in name only. The gate also
prints the whole coverage table, since that list is how the library gets
labelled in the first place.

Casting uses it: only composable scenes are drawn as overlays, and because what
goes on top is now guaranteed to leave the shot underneath visible, layering
happens far more often — the overlay chance went from 0.12 to 0.3 at its base.

The gallery shows coverage and surface beside each score, sorts by sparsest, and
says in the header why the two numbers explain each other.

Nothing is labelled `composable` yet. Every scene keeps the behaviour it had
through `surfaceOf`, which defaults to canvas unless the scene is an accent, so
this commit changes the mechanism and not the output. The coverage table is what
the labelling should be read off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dejvino
2026-08-18 18:20:24 +02:00
co-authored by Claude Opus 5
parent c099501ead
commit eda8ac3810
5 changed files with 143 additions and 7 deletions
+17 -1
View File
@@ -17,7 +17,7 @@
import { Engine } from '../engine/Engine.js';
import { scenes } from '../scenes/registry.js';
import { sampleValues } from '../params/schema.js';
import { sampleValues, surfaceOf } from '../params/schema.js';
import { Rng, hashString } from '../engine/rng.js';
import { featureProviderFor } from '../audio/FeatureTrack.js';
import { songBank } from '../audio/songbank.js';
@@ -150,6 +150,20 @@ export function renderScene(engine, module, contexts) {
}
for (const b of Object.keys(byBlock)) byBlock[b] /= pairs || 1;
// How much of the frame this scene paints, averaged over the six. Free —
// the pixels are already here — and it is the number that explains the
// ranking: a scene covering 2% of the frame is a few bright things on
// black, which scores well for variety and is thin to watch on its own.
// Whether that is a problem depends on whether it is ever layered.
let covered = 0;
for (const px of thumbs) {
let lit = 0;
for (let i = 0; i < px.length; i += 4) {
if (px[i] + px[i + 1] + px[i + 2] > 90) lit++;
}
covered += lit / (px.length / 4) / thumbs.length;
}
// A dead shader scores zero on every block, which is indistinguishable from
// a very boring scene if you only read the number — and it happened: the
// subject helpers were declared above the ink they call, the whole preamble
@@ -162,6 +176,8 @@ export function renderScene(engine, module, contexts) {
return {
thumbs,
variety: pairs ? total / pairs : 0,
coverage: covered,
surface: surfaceOf(module),
byBlock,
error: dead
? `renders nothing — luminance ${lum.toFixed(4)}, variance ${variance.toFixed(4)}. ` +