From 7736390f9c7cc18cee2cec781ca7e19350973f63 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sun, 13 Sep 2026 19:42:50 +0200 Subject: [PATCH] Comfort: agitation ceiling, composition validator and whole-screen caps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- flow-state/.claude/settings.json | 7 + flow-state/filmstrip.html | 36 ++- flow-state/gallery.html | 47 +++- flow-state/src/checks/filmstrip.js | 44 ++++ flow-state/src/checks/gallery.js | 111 +++++++++ flow-state/src/checks/metadata.js | 42 +++- flow-state/src/checks/scene-gate.js | 23 ++ flow-state/src/checks/scene-metrics.js | 109 +++++++++ flow-state/src/engine/Layer.js | 35 ++- flow-state/src/engine/agitation.js | 217 ++++++++++++++++++ flow-state/src/look/LookGenerator.js | 180 ++++++++++++++- flow-state/src/look/comfort.js | 166 ++++++++++++++ flow-state/src/look/composition-validator.js | 113 +++++++++ flow-state/src/look/directors.js | 24 ++ flow-state/src/look/scene-filter.js | 81 +++++++ flow-state/src/main.js | 9 +- flow-state/src/scenes/shader/analog-wow.js | 9 +- flow-state/src/scenes/shader/block-mosh.js | 11 +- flow-state/src/scenes/shader/gate-corridor.js | 8 +- flow-state/src/scenes/shader/metaballs.js | 8 +- flow-state/src/scenes/shader/pitch-shatter.js | 9 +- flow-state/src/scenes/shader/scale-mosaic.js | 8 +- flow-state/src/scenes/shader/scan-tear.js | 18 +- flow-state/src/scenes/shader/tide-rings.js | 9 +- flow-state/tools/lint-scenes.js | 44 ++++ 25 files changed, 1319 insertions(+), 49 deletions(-) create mode 100644 flow-state/.claude/settings.json create mode 100644 flow-state/src/checks/scene-metrics.js create mode 100644 flow-state/src/engine/agitation.js create mode 100644 flow-state/src/look/comfort.js create mode 100644 flow-state/src/look/composition-validator.js create mode 100644 flow-state/src/look/scene-filter.js diff --git a/flow-state/.claude/settings.json b/flow-state/.claude/settings.json new file mode 100644 index 0000000..f65aa61 --- /dev/null +++ b/flow-state/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run lint:scenes *)" + ] + } +} diff --git a/flow-state/filmstrip.html b/flow-state/filmstrip.html index 3f11ee3..8718e24 100644 --- a/flow-state/filmstrip.html +++ b/flow-state/filmstrip.html @@ -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 @@ @@ -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 ? '' : + ` ` + + ` ` + + `agit ${agit.toFixed(2)} · flash ${(row.agitationFlash ?? 0).toFixed(1)}/s · motion ${((row.agitationMotion ?? 0)*100).toFixed(0)}% · struct ${(row.agitationStructPeak ?? row.agitationStruct ?? row.peakStructure ?? 0).toFixed(2)}` + + ``; el.innerHTML = `
${row.name} watch ▸ ${row.bpm ?? '?'} bpm · ${row.sections ?? '?'} sections · ${row.director || ''} - direction ${(row.direction ?? 0).toFixed(2)} · drift ${(row.drift ?? 0).toFixed(3)} + direction ${(row.direction ?? 0).toFixed(2)} · drift ${(row.drift ?? 0).toFixed(3)}${agitLabel} ${row.error ? `${row.error}` : (row.storyLine || '')}
`; @@ -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()}` : ''); } diff --git a/flow-state/gallery.html b/flow-state/gallery.html index 0912bc8..6d3d241 100644 --- a/flow-state/gallery.html +++ b/flow-state/gallery.html @@ -44,6 +44,12 @@ .row.thin { border-color: #eab308; } .row.good { border-color: #22c55e; } .row.broken { border-color: #a855f7; background: #150e1c; } + .row.agitated { border-color: #f97316; } + .comfort { color: #9aa3b5; font-size: 11px; } + .comfort .meter i.low { background: #22c55e; } + .comfort .meter i.mid { background: #eab308; } + .comfort .meter i.high { background: #ef4444; } + .comfort .meter i.hot { background: #f97316; } .head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 6px; flex-wrap: wrap; } .name { font-size: 14px; color: #e6eaf2; } .fam { color: #6b7280; } @@ -132,6 +138,7 @@ +