A gallery, and a debug index to reach it from
Sixty-five visualizers, six renders each, one per song — each carrying that song's whole identity: its cast, ink, lattice, palette and a fresh parameter draw. Side by side, a scene that cannot be changed by the song is obvious in a way no aggregate could show, which is the complaint this answers. It scores as well as shows. Each row carries the mean structural distance between its own six frames, on the same descriptor the variety harness uses and with colour excluded, so six palettes cannot disguise one image. Sorted least-varied first, because browsing sixty-five scenes hunting for the repetitive ones is precisely what a sort order should do for you. The result names names. Thirteen scenes barely change across six songs, and they fall into two groups that were already known separately. The ink-only migrations — Analog Wow, Halftone Misprint, Pitch Shatter, Block Mosh, Scan Tear — are the shallow tier flagged in MIGRATION.md, where the whole migration was one posterisation. And Moiré Grid, Isometric Blocks, Quasicrystal, Truchet Fold, Voronoi Shatter and Apollonian Gasket are the structural twin cliques the library sweep found weeks of measurement ago, arriving here by a completely different route: the sweep compared scenes to each other, the gallery compares a scene to itself, and they agree on the same offenders. debug.html collects the tools, since there are now enough of them that knowing which to open is its own problem. It also carries the two things a newcomer would otherwise learn the hard way: which measurements are safe to steer by (direct render comparisons) and which are not (aggregate ratios), and what to do when a page renders perfectly and does nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4217cca3d3
commit
c6d96c9d40
183
flow-state/debug.html
Normal file
183
flow-state/debug.html
Normal file
@ -0,0 +1,183 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>flow-state · debug</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
body {
|
||||
margin: 0; padding: 24px 24px 80px;
|
||||
background: #0b0d12; color: #d6dae3;
|
||||
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
h1 { font-size: 15px; letter-spacing: .12em; text-transform: uppercase; color: #7d8698; margin: 0 0 6px; }
|
||||
h2 { font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: #6b7280; margin: 26px 0 10px; font-weight: 600; }
|
||||
p.lede { color: #8b94a7; max-width: 80ch; margin: 0 0 8px; }
|
||||
a { color: #7dd3fc; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 10px; }
|
||||
.card {
|
||||
background: #11141b; border: 1px solid #1c2030; border-radius: 4px;
|
||||
padding: 12px 14px; border-left: 3px solid #2a3040;
|
||||
}
|
||||
.card.slow { border-left-color: #eab308; }
|
||||
.card.fast { border-left-color: #22c55e; }
|
||||
.card.cli { border-left-color: #6366f1; }
|
||||
.card h3 { margin: 0 0 4px; font-size: 13px; }
|
||||
.card p { margin: 0 0 8px; color: #8b94a7; font-size: 12px; }
|
||||
.card .meta { color: #565f70; font-size: 11px; }
|
||||
code, pre {
|
||||
background: #080a0f; border: 1px solid #1a1e28; border-radius: 3px;
|
||||
padding: 1px 5px; color: #b8c0d0; font-size: 12px;
|
||||
}
|
||||
pre { padding: 8px 10px; overflow-x: auto; margin: 6px 0 0; }
|
||||
.note { color: #6b7280; font-size: 12px; max-width: 80ch; }
|
||||
.warn { color: #eab308; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>flow-state · debug</h1>
|
||||
<p class="lede">
|
||||
Everything here answers a question the app itself cannot. The app shows you
|
||||
one video; these show you whether the generator is doing its job across
|
||||
many.
|
||||
</p>
|
||||
|
||||
<h2>Looking at output</h2>
|
||||
<div class="grid">
|
||||
<div class="card fast">
|
||||
<h3><a href="/gallery.html">gallery</a></h3>
|
||||
<p>Every visualizer, six times, on six different songs' content — cast,
|
||||
ink, lattice, palette and parameters all varying. Sorted least-varied
|
||||
first, so the scenes that always look the same come to the top.</p>
|
||||
<div class="meta">~3 min · the one to open when a scene feels familiar</div>
|
||||
</div>
|
||||
<div class="card fast">
|
||||
<h3><a href="/">the app</a></h3>
|
||||
<p>Drop a track and watch it. <code>test/songs/*.wav</code> holds the
|
||||
synthetic bank if you want something with known properties.</p>
|
||||
<div class="meta">build the bank with <code>npm run build:songs</code></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Is it correct?</h2>
|
||||
<div class="grid">
|
||||
<div class="card fast">
|
||||
<h3><a href="/checks.html">phase gates</a></h3>
|
||||
<p>Every acceptance gate in <code>PLAN.md</code>, run for real. This is
|
||||
the one to check before committing.</p>
|
||||
<div class="meta">
|
||||
<a href="/checks.html?slow=1">?slow=1</a> full suite ·
|
||||
<a href="/checks.html?phase=12">?phase=12</a> one phase
|
||||
</div>
|
||||
</div>
|
||||
<div class="card fast">
|
||||
<h3>single scene</h3>
|
||||
<p>The whole per-scene battery for one visualizer: renders, animates,
|
||||
deterministic, distinct, param sweep, flash rate, and every declared
|
||||
trait and artifact.</p>
|
||||
<div class="meta"><code>checks.html?scene=Metaballs</code> — the loop you
|
||||
are in while writing or migrating one</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Is it varied?</h2>
|
||||
<p class="note">
|
||||
These measure whether two videos differ, which no other gate can see — a
|
||||
generator that ignores its input passes determinism and liveness perfectly.
|
||||
<span class="warn">Read the caution below before trusting a single run.</span>
|
||||
</p>
|
||||
<div class="grid">
|
||||
<div class="card slow">
|
||||
<h3><a href="/checks.html?decompose=1">decompose</a></h3>
|
||||
<p>Identity against container, measured apart: hold the scene fixed and
|
||||
vary the song's content, then the reverse. <strong>The most
|
||||
trustworthy number here</strong> — it compares renders directly, and
|
||||
its noise floor is exactly zero.</p>
|
||||
<div class="meta">~1 min</div>
|
||||
</div>
|
||||
<div class="card slow">
|
||||
<h3><a href="/checks.html?songs=1&count=12">song variety</a></h3>
|
||||
<p>Twelve songs from the bank, each with its own audio-derived seed. Also
|
||||
reports coupling — whether songs that <em>sound</em> different come
|
||||
out <em>looking</em> different, which is still unsolved.</p>
|
||||
<div class="meta">~2 min · <a href="/checks.html?variety=1">?variety=1</a>
|
||||
for the seed version, one song and many seeds</div>
|
||||
</div>
|
||||
<div class="card slow">
|
||||
<h3><a href="/checks.html?experiment=1&count=12&repeats=3">arms, with error bars</a></h3>
|
||||
<p>Three pools compared over repeated runs. The template for any A/B
|
||||
here: it prints the difference against its own noise band and says
|
||||
outright when the two are indistinguishable.</p>
|
||||
<div class="meta">~5 min</div>
|
||||
</div>
|
||||
<div class="card slow">
|
||||
<h3><a href="/checks.html?sweep=1&count=12&sizes=4,8,16,32&repeats=3">pool sweep</a></h3>
|
||||
<p>How many scenes a track should draw on. Swept rather than guessed —
|
||||
and the answer turned out to be that it does not matter between 4 and
|
||||
32.</p>
|
||||
<div class="meta">~5 min</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Command line</h2>
|
||||
<div class="grid">
|
||||
<div class="card cli">
|
||||
<h3>migration status</h3>
|
||||
<p>Which scenes draw the song's content, which still draw their own, and
|
||||
what each remaining one needs. Recipe in <code>MIGRATION.md</code>.</p>
|
||||
<pre>node tools/migration-status.js</pre>
|
||||
</div>
|
||||
<div class="card cli">
|
||||
<h3>cast census</h3>
|
||||
<p>Which visualizers actually get cast, across songs and seeds — and for
|
||||
the ones that do not, which of the four gates killed them.</p>
|
||||
<pre>node tools/cast-census.js 12 40</pre>
|
||||
</div>
|
||||
<div class="card cli">
|
||||
<h3>identity census</h3>
|
||||
<p>How far apart the songs' identities are before anything is rendered.
|
||||
No GPU. Run this before blaming the visualizers.</p>
|
||||
<pre>node tools/identity-census.js 12</pre>
|
||||
</div>
|
||||
<div class="card cli">
|
||||
<h3>song bank</h3>
|
||||
<p>Rebuild the test songs and verify they still span every feature the
|
||||
generator reads. Fails loudly if an axis has collapsed.</p>
|
||||
<pre>npm run build:songs
|
||||
npm run check:songs</pre>
|
||||
</div>
|
||||
<div class="card cli">
|
||||
<h3>static gates</h3>
|
||||
<p>Determinism grep, schema/shader agreement both ways, and the backtick
|
||||
check that guards the shader template literals.</p>
|
||||
<pre>npm run lint:scenes
|
||||
npm test</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Reading the variety numbers</h2>
|
||||
<p class="note">
|
||||
This harness has two classes of measurement and only one of them is safe to
|
||||
steer by. <strong>Direct render comparisons</strong> — the gallery score,
|
||||
<code>decompose</code>, the per-scene gate — have a noise floor of zero and
|
||||
mean what they say. <strong>Aggregate ratios</strong> — chiefly
|
||||
<code>separation</code>, which divides one small difference by another —
|
||||
swing wildly between runs: three runs of the same measurement gave 0.31,
|
||||
0.56 and 0.07. Four conclusions were drawn and withdrawn during this epic
|
||||
for exactly that reason. Anything under about 0.01 of spread needs
|
||||
<code>&repeats=3</code> before it is believed, and a difference that
|
||||
changes sign with the sample is not a difference.
|
||||
</p>
|
||||
|
||||
<h2>If a page is dead</h2>
|
||||
<p class="note">
|
||||
A module that fails to load takes the whole graph with it: the page renders,
|
||||
every control is present, and nothing is wired to anything. The app carries a
|
||||
boot guard that says so. The usual causes are a content blocker — a file
|
||||
named like tracking will be treated as tracking, which cost this project a
|
||||
session over a file called <code>clicktrack.js</code> — or a dev server
|
||||
caught mid-restart.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
161
flow-state/gallery.html
Normal file
161
flow-state/gallery.html
Normal file
@ -0,0 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>flow-state · gallery</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
body {
|
||||
margin: 0; padding: 20px 24px 60px;
|
||||
background: #0b0d12; color: #d6dae3;
|
||||
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
h1 { font-size: 15px; letter-spacing: .12em; text-transform: uppercase; color: #7d8698; margin: 0 0 4px; }
|
||||
a { color: #7dd3fc; }
|
||||
.lede { color: #8b94a7; max-width: 78ch; margin: 0 0 14px; }
|
||||
.bar { position: sticky; top: 0; z-index: 5; background: #0b0d12; padding: 10px 0 12px; border-bottom: 1px solid #1c2030; margin-bottom: 16px; }
|
||||
button, select {
|
||||
background: #171b24; color: #d6dae3; border: 1px solid #2a3040;
|
||||
padding: 5px 10px; font: inherit; border-radius: 3px; cursor: pointer;
|
||||
}
|
||||
button:hover { border-color: #4a5468; }
|
||||
#status { color: #8b94a7; margin-left: 10px; }
|
||||
.ctx { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0 0; font-size: 11px; color: #6b7280; }
|
||||
.ctx span { background: #11141b; padding: 3px 7px; border-radius: 3px; }
|
||||
|
||||
.row { margin-bottom: 22px; border-left: 3px solid #232838; padding-left: 12px; }
|
||||
.row.flat { border-color: #ef4444; }
|
||||
.row.thin { border-color: #eab308; }
|
||||
.row.good { border-color: #22c55e; }
|
||||
.head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 6px; flex-wrap: wrap; }
|
||||
.name { font-size: 14px; color: #e6eaf2; }
|
||||
.fam { color: #6b7280; }
|
||||
.score { color: #9aa3b5; }
|
||||
.meter { display: inline-block; width: 130px; height: 7px; background: #1a1e28; border-radius: 4px; overflow: hidden; vertical-align: middle; }
|
||||
.meter i { display: block; height: 100%; background: #4ade80; }
|
||||
.row.flat .meter i { background: #ef4444; }
|
||||
.row.thin .meter i { background: #eab308; }
|
||||
.blocks { color: #5c6577; font-size: 11px; }
|
||||
.thumbs { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; }
|
||||
.thumbs figure { margin: 0; }
|
||||
.thumbs canvas { width: 100%; display: block; background: #05070a; border-radius: 2px; }
|
||||
.thumbs figcaption { font-size: 10px; color: #565f70; margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.err { color: #f87171; }
|
||||
@media (max-width: 1100px) { .thumbs { grid-template-columns: repeat(3, 1fr); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>flow-state · gallery</h1>
|
||||
<p class="lede">
|
||||
Every visualizer rendered six times, once per song, each with that song's
|
||||
complete identity — its cast, ink, lattice, palette and sampled parameters.
|
||||
A scene whose six frames are interchangeable is one the song cannot change.
|
||||
Sorted least-varied first, scored on the same structural descriptor the
|
||||
variety harness uses, with colour excluded so six palettes cannot disguise
|
||||
one image. <a href="/debug.html">← all debug tools</a>
|
||||
</p>
|
||||
|
||||
<div class="bar">
|
||||
<button id="run">build gallery</button>
|
||||
<select id="sort">
|
||||
<option value="variety">sort: least varied first</option>
|
||||
<option value="name">sort: by name</option>
|
||||
<option value="family">sort: by family</option>
|
||||
</select>
|
||||
<span id="status">idle</span>
|
||||
<div class="ctx" id="ctx"></div>
|
||||
</div>
|
||||
|
||||
<div id="out"></div>
|
||||
|
||||
<script type="module">
|
||||
import { buildGallery, galleryContexts, blit, THUMB } from '/src/checks/gallery.js';
|
||||
|
||||
const out = document.getElementById('out');
|
||||
const status = document.getElementById('status');
|
||||
const sortSel = document.getElementById('sort');
|
||||
let rows = [];
|
||||
let contexts = [];
|
||||
|
||||
// A scene that changes very little across six songs is the thing this page
|
||||
// exists to surface, so the thresholds are set where "I have seen this before"
|
||||
// starts rather than at anything statistical.
|
||||
const grade = (v) => (v < 0.05 ? 'flat' : v < 0.10 ? 'thin' : 'good');
|
||||
|
||||
function draw() {
|
||||
const mode = sortSel.value;
|
||||
const sorted = rows.slice().sort((a, b) => (
|
||||
mode === 'name' ? a.module.name.localeCompare(b.module.name)
|
||||
: mode === 'family' ? (a.module.family.localeCompare(b.module.family)
|
||||
|| a.variety - b.variety)
|
||||
: a.variety - b.variety));
|
||||
|
||||
out.innerHTML = '';
|
||||
for (const row of sorted) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `row ${grade(row.variety)}`;
|
||||
const blocks = Object.entries(row.byBlock)
|
||||
.map(([k, v]) => `${k} ${v.toFixed(3)}`).join(' · ');
|
||||
el.innerHTML = `
|
||||
<div class="head">
|
||||
<span class="name">${row.module.name}</span>
|
||||
<span class="fam">${row.module.family}${row.module.role === 'accent' ? ' · accent' : ''}</span>
|
||||
<span class="score">
|
||||
<span class="meter"><i style="width:${Math.min(100, row.variety * 500)}%"></i></span>
|
||||
${row.variety.toFixed(3)}
|
||||
</span>
|
||||
<span class="blocks">${row.error ? `<span class="err">${row.error}</span>` : blocks}</span>
|
||||
</div>
|
||||
<div class="thumbs"></div>`;
|
||||
const grid = el.querySelector('.thumbs');
|
||||
row.thumbs.forEach((pixels, i) => {
|
||||
const fig = document.createElement('figure');
|
||||
const canvas = document.createElement('canvas');
|
||||
blit(canvas, pixels, THUMB.width, THUMB.height);
|
||||
fig.appendChild(canvas);
|
||||
const cap = document.createElement('figcaption');
|
||||
cap.textContent = contexts[i] ? contexts[i].name : '';
|
||||
cap.title = contexts[i] ? contexts[i].identity : '';
|
||||
fig.appendChild(cap);
|
||||
grid.appendChild(fig);
|
||||
});
|
||||
out.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
sortSel.addEventListener('change', draw);
|
||||
|
||||
document.getElementById('run').addEventListener('click', async () => {
|
||||
const button = document.getElementById('run');
|
||||
button.disabled = true;
|
||||
rows = [];
|
||||
out.innerHTML = '';
|
||||
|
||||
status.textContent = 'analysing six songs…';
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
contexts = galleryContexts(6);
|
||||
document.getElementById('ctx').innerHTML = contexts
|
||||
.map((c) => `<span title="${c.identity}">${c.name}</span>`).join('');
|
||||
|
||||
const started = Date.now();
|
||||
await buildGallery({
|
||||
contexts,
|
||||
onScene: (done, total, name, row) => {
|
||||
rows.push(row);
|
||||
status.textContent = `${done}/${total} · ${name}`;
|
||||
// Redraw periodically rather than every scene: sorting the list on
|
||||
// every row would reflow sixty times for no benefit.
|
||||
if (done % 8 === 0 || done === total) draw();
|
||||
},
|
||||
});
|
||||
|
||||
const flat = rows.filter((r) => r.variety < 0.05);
|
||||
status.textContent = `${rows.length} scenes in ${((Date.now() - started) / 1000).toFixed(0)}s · ` +
|
||||
`${flat.length} barely change across songs`;
|
||||
draw();
|
||||
button.disabled = false;
|
||||
window.__GALLERY__ = rows.map((r) => ({ name: r.module.name, variety: r.variety, error: r.error }));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
156
flow-state/src/checks/gallery.js
Normal file
156
flow-state/src/checks/gallery.js
Normal file
@ -0,0 +1,156 @@
|
||||
// The gallery: every visualizer, six times, on six different songs' content.
|
||||
//
|
||||
// The complaint this answers is one a metric could not raise — watching a few
|
||||
// videos, certain scenes announce themselves. You have seen that one before.
|
||||
// You cannot tell which ones from a number, because the harness measures whole
|
||||
// videos and a scene that always looks like itself is averaged in with
|
||||
// everything around it.
|
||||
//
|
||||
// So this renders each scene under six complete identities — six casts, six ink
|
||||
// treatments, six lattices, six palettes, six parameter draws — and puts them
|
||||
// side by side. A scene whose six thumbnails are interchangeable is a scene the
|
||||
// song cannot change, and that is the definition of the problem.
|
||||
//
|
||||
// Then it scores them, using the same structural descriptor the variety harness
|
||||
// runs on, so the gallery can be sorted worst-first. Browsing sixty-six scenes
|
||||
// looking for the repetitive ones is exactly the job a sort order should do.
|
||||
|
||||
import { Engine } from '../engine/Engine.js';
|
||||
import { scenes } from '../scenes/registry.js';
|
||||
import { sampleValues } from '../params/schema.js';
|
||||
import { Rng, hashString } from '../engine/rng.js';
|
||||
import { featureProviderFor } from '../audio/FeatureTrack.js';
|
||||
import { songBank } from '../audio/songbank.js';
|
||||
import { generateLook } from '../look/LookGenerator.js';
|
||||
import { describeIdentity } from '../look/Identity.js';
|
||||
import { frameDescriptor } from './variety/descriptors.js';
|
||||
import { descriptorDistance, STRUCTURAL } from './variety/signature.js';
|
||||
|
||||
const THUMB = { width: 256, height: 144 };
|
||||
|
||||
/**
|
||||
* Six contexts, one per song: everything a scene is handed when it is cast.
|
||||
*
|
||||
* Built from real bank entries rather than invented, so what the gallery shows
|
||||
* is what the generator would actually produce — the same identities, palettes
|
||||
* and section biases, only with the scene held fixed instead of chosen.
|
||||
*/
|
||||
export function galleryContexts(count = 6) {
|
||||
return songBank({ count }).map((entry) => {
|
||||
const look = generateLook(entry.track, { seed: hashString(entry.name) });
|
||||
// The busiest section, because that is where a scene is asked for the
|
||||
// most and where two scenes are most likely to converge.
|
||||
const section = look.sections.reduce(
|
||||
(best, s) => (s.bias.energy > best.bias.energy ? s : best), look.sections[0]);
|
||||
return {
|
||||
name: entry.name,
|
||||
track: entry.track,
|
||||
palette: look.palette,
|
||||
personality: look.personality,
|
||||
bias: section.bias,
|
||||
frame: section.startFrame + Math.floor((section.endFrame - section.startFrame) * 0.5),
|
||||
identity: describeIdentity(look.personality.identity),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Bottom-up WebGL pixels into a canvas the right way up. */
|
||||
function blit(canvas, pixels, width, height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const image = ctx.createImageData(width, height);
|
||||
const row = width * 4;
|
||||
for (let y = 0; y < height; y++) {
|
||||
const src = (height - 1 - y) * row;
|
||||
image.data.set(pixels.subarray(src, src + row), y * row);
|
||||
}
|
||||
ctx.putImageData(image, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one scene across every context.
|
||||
*
|
||||
* @returns {{thumbs: Uint8Array[], variety: number, byBlock: object}}
|
||||
*/
|
||||
export function renderScene(engine, module, contexts) {
|
||||
const thumbs = [];
|
||||
const descriptors = [];
|
||||
|
||||
for (const ctx of contexts) {
|
||||
engine.timeline.setDuration(ctx.track.duration);
|
||||
engine.setFeatureProvider(featureProviderFor(ctx.track));
|
||||
|
||||
const rng = new Rng(hashString(`${module.name}:${ctx.name}`));
|
||||
const params = sampleValues(module, rng, ctx.bias, ctx.personality.temperament);
|
||||
|
||||
engine.setLayerSpecs([{
|
||||
module,
|
||||
params,
|
||||
seed: rng.int(0, 0x7fffffff),
|
||||
opacity: 1,
|
||||
blend: 'normal',
|
||||
palette: ctx.palette,
|
||||
personality: ctx.personality,
|
||||
}]);
|
||||
engine.compositor.reset();
|
||||
// A few frames of warm-up so anything with state is past its first frame.
|
||||
for (let f = ctx.frame - 6; f < ctx.frame; f++) engine.renderFrame(f);
|
||||
const pixels = Uint8Array.from(engine.readPixels(engine.renderFrame(ctx.frame)));
|
||||
|
||||
thumbs.push(pixels);
|
||||
descriptors.push(frameDescriptor(pixels, THUMB.width, THUMB.height));
|
||||
}
|
||||
|
||||
// How different this scene's six outputs are from each other, on the same
|
||||
// structural descriptor the variety harness uses — colour excluded, because
|
||||
// six palettes would otherwise make every scene look varied.
|
||||
const byBlock = {};
|
||||
let total = 0, pairs = 0;
|
||||
for (let i = 0; i < descriptors.length; i++) {
|
||||
for (let j = i + 1; j < descriptors.length; j++) {
|
||||
const d = descriptorDistance(descriptors[i], descriptors[j]);
|
||||
for (const b of [...STRUCTURAL, 'colour']) byBlock[b] = (byBlock[b] || 0) + (d[b] || 0);
|
||||
total += STRUCTURAL.reduce((a, b) => a + (d[b] || 0), 0) / STRUCTURAL.length;
|
||||
pairs++;
|
||||
}
|
||||
}
|
||||
for (const b of Object.keys(byBlock)) byBlock[b] /= pairs || 1;
|
||||
|
||||
return { thumbs, variety: pairs ? total / pairs : 0, byBlock };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the whole gallery, reporting progress as it goes.
|
||||
*
|
||||
* @param {(done:number, of:number, name:string, row:object) => void} onScene
|
||||
*/
|
||||
export async function buildGallery({ contexts, onScene, only = null } = {}) {
|
||||
const list = only
|
||||
? scenes.filter((m) => only.includes(m.name))
|
||||
: scenes.filter((m) => m.kind === 'fragment');
|
||||
|
||||
const engine = new Engine({ ...THUMB });
|
||||
const rows = [];
|
||||
try {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const module = list[i];
|
||||
let row;
|
||||
try {
|
||||
row = { module, ...renderScene(engine, module, contexts) };
|
||||
} catch (err) {
|
||||
row = { module, thumbs: [], variety: 0, byBlock: {}, error: err.message };
|
||||
}
|
||||
rows.push(row);
|
||||
if (onScene) onScene(i + 1, list.length, module.name, row);
|
||||
// Yield so the page can paint each row as it lands rather than
|
||||
// freezing for a minute and then showing everything at once.
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
}
|
||||
} finally {
|
||||
engine.dispose();
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
export { blit, THUMB };
|
||||
@ -11,6 +11,8 @@ export default defineConfig({
|
||||
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'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user