Comfort: agitation ceiling, composition validator and whole-screen caps
Whole-screen flashing/coherent motion is now a gated comfort signal rather than a hunt-and-tweak. Three-arm agitation (flash/3, tile-motion/0.32, structure/0.12) over a 1s floating window, worst window wins — engine/agitation.js. Policy centralised in look/comfort.js: director comfort appetite, per-scene agitationOf proxy, agitationWeight lean and stackAgitation. Canvas beat/flux capped in Layer.resolveParams (0.15/0.25, spike→smooth) so one contract change tames every full-frame scene; lint warns above the cap. Selection now filters before casting (look/scene-filter.js, 12+ ranged metrics from checks/scene-metrics.js, SCHEMA 7) and validates after stacking (look/composition-validator.js) with a deterministic retune: drop overlay → swap quiet composable → reselect filtered shot. Fix validator attempt-3 ground-layer bug that crashed ?song=dusk (LookGenerator now passes module, not layer). Eight canvas shaders window their beat/flux (focusField + per-seed hash mask) and report the scaled amount to Layer so the cap stays authoritative: scan-tear, gate-corridor, analog-wow, block-mosh, metaballs, pitch-shatter, scale-mosaic, tide-rings. Checks: scene-gate comfort gate (agitation ≤ 0.70), filmstrip/gallery meters + per-scene drawer, metadata SCHEMA 7 with ranged metrics and fingerprint over agitation/comfort sources. main.js surfaces COMPOSITION_REFUSED as a user-facing error. Lint: 114 files clean, 71 scenes ok. All 17 songs generate looks. Build passes. metadata.json refresh deferred to gallery → Refresh metadata (SCHEMA 7 writer in vite.config). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,15 @@
|
||||
.row.churn { border-color: #eab308; }
|
||||
.row.arc { border-color: #22c55e; }
|
||||
.row.broken { border-color: #a855f7; background: #150e1c; }
|
||||
.row.agitated { border-color: #f97316; background: #1a1210; }
|
||||
.agit { color: #9aa3b5; font-size: 11px; }
|
||||
.agit .meter { display: inline-block; width: 96px; height: 7px; background: #1a1e28; border-radius: 4px; vertical-align: middle; margin-right: 6px; }
|
||||
.agit .meter i { display: block; height: 100%; border-radius: 4px 0 0 4px; }
|
||||
.agit .meter i.low { background: #22c55e; }
|
||||
.agit .meter i.mid { background: #eab308; }
|
||||
.agit .meter i.high { background: #f97316; }
|
||||
.agit b.agitated { color: #f97316; }
|
||||
.agit b.calm { color: #22c55e; }
|
||||
.head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 6px; flex-wrap: wrap; }
|
||||
.name { font-size: 14px; color: #e6eaf2; }
|
||||
.meta { color: #6b7280; font-size: 11px; }
|
||||
@@ -82,6 +91,7 @@
|
||||
<select id="sort">
|
||||
<option value="direction">sort: least direction first</option>
|
||||
<option value="drift">sort: least drift first</option>
|
||||
<option value="agitation">sort: most agitating first</option>
|
||||
<option value="name">sort: bank order</option>
|
||||
<option value="plot">sort: by plot</option>
|
||||
</select>
|
||||
@@ -118,9 +128,13 @@ let rows = [];
|
||||
|
||||
// Read against the arcless reference the variety report prints: around zero is
|
||||
// a video that is as unlike itself after ten seconds as after four minutes.
|
||||
const grade = (row) => (row.error ? 'broken'
|
||||
: row.direction < 0.15 ? 'static'
|
||||
: row.direction < 0.45 ? 'churn' : 'arc');
|
||||
const AGIT_CEILING = 0.70;
|
||||
const agitClass = (v) => (v == null ? 'low' : v >= AGIT_CEILING ? 'high' : v >= 0.50 ? 'mid' : 'low');
|
||||
const grade = (row) => {
|
||||
if (row.error) return 'broken';
|
||||
if ((row.agitation ?? 0) >= AGIT_CEILING) return 'agitated';
|
||||
return row.direction < 0.15 ? 'static' : row.direction < 0.45 ? 'churn' : 'arc';
|
||||
};
|
||||
|
||||
// Nine stills are an argument about a video, and the only reply to them is the
|
||||
// video. The app takes the bank's wav straight from `?song=`, and the seed is
|
||||
@@ -140,13 +154,19 @@ const watchLink = (name) =>
|
||||
function renderRow(row) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `row ${grade(row)}`;
|
||||
const agit = row.agitation ?? 0;
|
||||
const agitLabel = row.error ? '' :
|
||||
` <span class="agit" title="worst 1s window in busiest section — max(flash/3, motion/0.32, struct/0.12). Whole-screen reserved for specials; region motion fine. Ceiling ${AGIT_CEILING.toFixed(2)}">` +
|
||||
`<span class="meter"><i class="${agitClass(agit)}" style="width:${Math.min(100, agit*100)}%"></i></span> ` +
|
||||
`agit <b class="${agit >= AGIT_CEILING ? 'agitated' : 'calm'}">${agit.toFixed(2)}</b> · flash ${(row.agitationFlash ?? 0).toFixed(1)}/s · motion ${((row.agitationMotion ?? 0)*100).toFixed(0)}% · struct ${(row.agitationStructPeak ?? row.agitationStruct ?? row.peakStructure ?? 0).toFixed(2)}` +
|
||||
`</span>`;
|
||||
el.innerHTML = `
|
||||
<div class="head">
|
||||
<span class="name">${row.name}</span>
|
||||
<a class="watch" href="${watchLink(row.name)}" target="_blank"
|
||||
title="play this song in the app, on the seed this strip was rendered with">watch ▸</a>
|
||||
<span class="meta">${row.bpm ?? '?'} bpm · ${row.sections ?? '?'} sections · ${row.director || ''}</span>
|
||||
<span class="num">direction <b>${(row.direction ?? 0).toFixed(2)}</b> · drift <b>${(row.drift ?? 0).toFixed(3)}</b></span>
|
||||
<span class="num">direction <b>${(row.direction ?? 0).toFixed(2)}</b> · drift <b>${(row.drift ?? 0).toFixed(3)}</b>${agitLabel}</span>
|
||||
<span class="story">${row.error ? `<span class="err">${row.error}</span>` : (row.storyLine || '')}</span>
|
||||
</div>
|
||||
<div class="strip"></div>`;
|
||||
@@ -183,7 +203,8 @@ function draw() {
|
||||
const mode = sortSel.value;
|
||||
const order = songNames();
|
||||
const sorted = rows.slice().sort((a, b) => (
|
||||
mode === 'name' ? order.indexOf(a.name) - order.indexOf(b.name)
|
||||
mode === 'agitation' ? (b.agitation ?? 0) - (a.agitation ?? 0)
|
||||
: mode === 'name' ? order.indexOf(a.name) - order.indexOf(b.name)
|
||||
: mode === 'drift' ? a.drift - b.drift
|
||||
: mode === 'plot' ? ((a.story?.plot || '').localeCompare(b.story?.plot || '') || a.direction - b.direction)
|
||||
: a.direction - b.direction));
|
||||
@@ -196,10 +217,15 @@ sortSel.addEventListener('change', draw);
|
||||
|
||||
function summarise(built) {
|
||||
const flat = rows.filter((r) => !r.error && r.direction < 0.15).length;
|
||||
const validatedNote = rows.some((r) => r.validatedNote) ? ' · ' + rows.filter((r) => r.validatedNote).length + ' re-tuned' : '';
|
||||
const agited = rows.filter((r) => (r.agitation ?? 0) >= AGIT_CEILING).length;
|
||||
const meanDir = rows.length
|
||||
? rows.reduce((a, r) => a + (r.direction || 0), 0) / rows.length : 0;
|
||||
const meanAgit = rows.length
|
||||
? rows.reduce((a, r) => a + (r.agitation ?? 0), 0) / rows.length : 0;
|
||||
return `${rows.length} songs · every ${every}s of ${duration}s · ` +
|
||||
`mean direction ${meanDir.toFixed(2)} · ${flat} with no direction` +
|
||||
` · mean agit ${meanAgit.toFixed(2)} · ${agited} agitated (≥${AGIT_CEILING.toFixed(2)})${validatedNote}` +
|
||||
(built ? ` · built ${new Date(built).toLocaleString()}` : '');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user