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:
@@ -25,6 +25,7 @@ import { galleryContexts, renderScene, THUMB } from './gallery.js';
|
||||
import { readsHistory } from '../params/schema.js';
|
||||
import { GROUND_BIAS, groundTemperamentFrom, groundPersonalityFrom } from '../scenes/surface.js';
|
||||
import metadata from '../scenes/metadata.json';
|
||||
import { METRIC_KEYS } from './scene-metrics.js';
|
||||
|
||||
/**
|
||||
* What invalidates the measurements.
|
||||
@@ -45,8 +46,16 @@ const SOURCES = {
|
||||
...import.meta.glob('/src/checks/variety/descriptors.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/checks/variety/signature.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/checks/gallery.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/checks/scene-metrics.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/checks/metadata.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/engine/shader-contract.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/engine/agitation.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/engine/flash.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/engine/Layer.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/look/comfort.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/look/scene-filter.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/look/composition-validator.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/scenes/surface.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/look/Identity.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/look/Personality.js', { query: '?raw', import: 'default', eager: true }),
|
||||
...import.meta.glob('/src/look/palette.js', { query: '?raw', import: 'default', eager: true }),
|
||||
@@ -55,7 +64,7 @@ const SOURCES = {
|
||||
};
|
||||
|
||||
/** The version of the measurement itself. Bump to force a refresh of everything. */
|
||||
export const SCHEMA = 5;
|
||||
export const SCHEMA = 7;
|
||||
|
||||
export function metricsFingerprint() {
|
||||
let h = 0x811c9dc5 >>> 0;
|
||||
@@ -133,8 +142,18 @@ export function measureLibrary({ onScene = null, contexts = null } = {}) {
|
||||
try {
|
||||
const list = scenes.filter((m) => m.kind === 'fragment' || m.kind === 'model');
|
||||
for (const module of list) {
|
||||
const { thumbs, variety, byBlock, descriptors, error } = renderScene(engine, module, ctx);
|
||||
const { thumbs, variety, byBlock, descriptors, error, agitation, flashPeak, motionPeak, motionMean, structPeak, structMean, metrics } = renderScene(engine, module, ctx);
|
||||
const bed = renderScene(engine, module, bedCtx);
|
||||
// Round ranged metrics to 3dp (4dp for agitation trio) — small, stable diffs
|
||||
const roundedMetrics = {};
|
||||
if (metrics) {
|
||||
for (const [k, r] of Object.entries(metrics)) {
|
||||
roundedMetrics[k] = {
|
||||
mean: round(r.mean, 3), min: round(r.min, 3), max: round(r.max, 3),
|
||||
std: round(r.std, 3), p10: round(r.p10, 3), p90: round(r.p90, 3),
|
||||
};
|
||||
}
|
||||
}
|
||||
out[module.name] = {
|
||||
coverage: round(thumbs.reduce((s, px) => s + litFraction(px), 0) / thumbs.length, 3),
|
||||
// The WORST of the six, not the mean. What a ground has to
|
||||
@@ -151,6 +170,16 @@ export function measureLibrary({ onScene = null, contexts = null } = {}) {
|
||||
variety: round(variety, 3),
|
||||
blocks: Object.fromEntries(
|
||||
Object.entries(byBlock).map(([b, v]) => [b, round(v, 3)])),
|
||||
// Legacy flat (busiest-context) — compat with existing comfort.js:agitationOf
|
||||
agitation: round(agitation || 0, 3),
|
||||
flashPeak: round(flashPeak || 0, 2),
|
||||
motionPeak: round(motionPeak || 0, 3),
|
||||
motionMean: round(motionMean || 0, 3),
|
||||
structPeak: round(structPeak || 0, 3),
|
||||
structMean: round(structMean || 0, 3),
|
||||
// Ranged metrics — 12+ keys × {mean,min,max,std,p10,p90}
|
||||
// Vast preliminary library the director filters before casting
|
||||
metrics: roundedMetrics,
|
||||
// Declared, not measured, and carried here anyway: it is a fact
|
||||
// about the scene that the composition rules read, and having
|
||||
// every compositional input in one file is the point.
|
||||
@@ -193,6 +222,15 @@ export function metadataDrift(fresh) {
|
||||
delta,
|
||||
});
|
||||
}
|
||||
const aNew = row.agitation ?? 0;
|
||||
const aWas = was.agitation ?? 0;
|
||||
const aDelta = aNew - aWas;
|
||||
if (Math.abs(aDelta) > 0.08) {
|
||||
moved.push({
|
||||
name, note: `agitation ${aWas.toFixed(2)} → ${aNew.toFixed(2)}`,
|
||||
delta: aDelta,
|
||||
});
|
||||
}
|
||||
}
|
||||
const gone = Object.keys(previous).filter((n) => !fresh.scenes[n]);
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user