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:
@@ -5,6 +5,12 @@ import {
|
||||
} from './shader-contract.js';
|
||||
import { signatureUniforms, NEUTRAL_UNIFORMS } from '../look/Personality.js';
|
||||
import { clampValue } from '../params/schema.js';
|
||||
import { surfaceOf } from '../scenes/surface.js';
|
||||
import {
|
||||
CANVAS_BEAT_CAP,
|
||||
CANVAS_FLUX_CAP,
|
||||
CANVAS_BEAT_SOFTEN_SPIKE,
|
||||
} from '../look/comfort.js';
|
||||
|
||||
export const BLEND_MODES = ['normal', 'add', 'screen', 'multiply', 'overlay', 'softlight'];
|
||||
|
||||
@@ -185,6 +191,16 @@ export class Layer {
|
||||
|
||||
if (!features) return out;
|
||||
|
||||
// A — whole-screen guard: canvas scenes (≥50% coverage) must not swerve
|
||||
// or jump with the beat. Every mid-tempo kick (≈2 Hz at 124 BPM) then
|
||||
// moves the ENTIRE view in sync — the nausea source on
|
||||
// ?song=centre&seed=3837488838. Only smaller regions/elements should
|
||||
// react wildly; they light few tiles. This is the immediate, central cap:
|
||||
// beat → ≤0.15 and spike→smooth, flux → ≤0.25. Composable scenes are
|
||||
// untouched. Tunable in look/comfort.js; stale metadata detected via
|
||||
// checks/metadata.js SOURCES. See engine/agitation.js for the tile metric.
|
||||
const isCanvas = surfaceOf(this.module) === 'canvas';
|
||||
|
||||
for (const [name, r] of Object.entries(reactive)) {
|
||||
const def = defs[name];
|
||||
if (!def || def.type === 'palette') continue;
|
||||
@@ -194,20 +210,31 @@ export class Layer {
|
||||
const raw = features[r.feature];
|
||||
if (raw === undefined) continue;
|
||||
|
||||
const shaped = applyResponse(Math.max(0, Math.min(1, raw)), r.response);
|
||||
let amount = r.amount;
|
||||
let response = r.response;
|
||||
if (isCanvas) {
|
||||
if (r.feature === 'beat') {
|
||||
amount = Math.min(amount, CANVAS_BEAT_CAP);
|
||||
if (CANVAS_BEAT_SOFTEN_SPIKE && response === 'spike') response = 'smooth';
|
||||
} else if (r.feature === 'flux') {
|
||||
amount = Math.min(amount, CANVAS_FLUX_CAP);
|
||||
}
|
||||
}
|
||||
|
||||
const shaped = applyResponse(Math.max(0, Math.min(1, raw)), response);
|
||||
const [lo, hi] = def.range || [0, 1];
|
||||
const span = hi - lo;
|
||||
const base = out[name] !== undefined ? out[name] : lo;
|
||||
|
||||
if (def.type === 'vec2') {
|
||||
out[name] = [
|
||||
clampValue(def, [base[0] + shaped * r.amount * span, 0])[0],
|
||||
clampValue(def, [0, base[1] + shaped * r.amount * span])[1],
|
||||
clampValue(def, [base[0] + shaped * amount * span, 0])[0],
|
||||
clampValue(def, [0, base[1] + shaped * amount * span])[1],
|
||||
];
|
||||
} else if (def.type === 'bool') {
|
||||
out[name] = base;
|
||||
} else {
|
||||
out[name] = clampValue(def, base + shaped * r.amount * span);
|
||||
out[name] = clampValue(def, base + shaped * amount * span);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
|
||||
Reference in New Issue
Block a user