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>
96 lines
4.1 KiB
JavaScript
96 lines
4.1 KiB
JavaScript
// Structural family: a corridor of nested gates receding to a vanishing point,
|
|
// travelled through.
|
|
//
|
|
// Rectilinear depth, which the library did not have: Kaleido Tunnel recedes but
|
|
// is radial and folded, Pylon Grid stands still, Neon City is front-on. Here the
|
|
// camera moves forward through a series of frames that scale up and pass, and
|
|
// the ring geometry is the track's signature form, so a hexagonal video travels
|
|
// through hexagonal gates.
|
|
//
|
|
// Motion is a saw on log-depth, which is what makes gates emerge from the
|
|
// vanishing point at a constant apparent rate instead of rushing at the end.
|
|
|
|
export const gateCorridor = {
|
|
name: 'Gate Corridor',
|
|
family: 'structural',
|
|
kind: 'fragment',
|
|
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
|
|
texture: 0.4,
|
|
consumes: ['cast', 'ink'],
|
|
traits: ['shape', 'camera', 'space', 'style'],
|
|
|
|
params: {
|
|
gates: { type: 'int', range: [3, 14], default: 8, uniform: 'u_gates', bias: 'density' , slowAxis: true },
|
|
aperture: { type: 'float', range: [0.15, 0.9], default: 0.45, uniform: 'u_aperture' },
|
|
thickness:{ type: 'float', range: [0.02, 0.3], default: 0.09, uniform: 'u_thickness' },
|
|
travel: { type: 'float', range: [0.02, 0.7], default: 0.2, uniform: 'u_travel', bias: 'motion', rate: true },
|
|
rails: { type: 'float', range: [0, 1], default: 0.4, uniform: 'u_rails' },
|
|
lamps: { type: 'float', range: [0, 1.4], default: 0.5, uniform: 'u_lamps', bias: 'energy' },
|
|
vanish: { type: 'float', range: [0, 0.5], default: 0.15, uniform: 'u_vanish' },
|
|
palette: { type: 'palette', count: 5 },
|
|
},
|
|
|
|
reactive: {
|
|
lamps: { feature: 'beat', amount: 0.14, response: 'smooth' },
|
|
aperture: { feature: 'bandLow', amount: 0.15, response: 'smooth' },
|
|
},
|
|
|
|
shader: `
|
|
vec4 scene(vec2 uv, vec2 p) {
|
|
float t = u_time * u_travel + u_seed;
|
|
p = sigCamera(p);
|
|
|
|
// The vanishing point sits on the track's horizon, slightly off centre.
|
|
vec2 vanishAt = vec2(sin(u_seed) * u_vanish, sigHorizonY() * 0.35);
|
|
vec2 q = p - vanishAt;
|
|
|
|
vec3 col = pal(0) * 0.04;
|
|
|
|
// Depth rails converging on the vanishing point.
|
|
if (u_rails > 0.01) {
|
|
float ang = atan(q.y, q.x);
|
|
float spokes = abs(fract(ang * 1.9098 + 0.5) - 0.5) * 2.0; // 12 rails
|
|
float rail = smoothstep(0.06, 0.0, spokes) * smoothstep(0.02, 0.5, length(q));
|
|
col += pal(1) * rail * u_rails * 0.25;
|
|
}
|
|
|
|
for (int i = 0; i < 14; i++) {
|
|
if (i >= u_gates) break;
|
|
float fi = float(i);
|
|
|
|
// Log-spaced depth with a saw: each gate walks forward, and when it
|
|
// passes the camera it wraps to the far end.
|
|
float phase = fract((fi / float(u_gates)) + t);
|
|
float scale = u_aperture * exp(phase * 3.2) * 0.35;
|
|
|
|
float d = abs(castMain(q / max(scale, 1e-3)) * max(scale, 1e-3));
|
|
|
|
// Near gates are drawn thicker and brighter: the only depth cue that
|
|
// matters once the geometry is right.
|
|
float near = phase;
|
|
float w = u_thickness * (0.25 + near * 1.2) * (0.5 + u_sigLine);
|
|
float frame = smoothstep(w, w * 0.25, d);
|
|
|
|
vec3 tint = palRamp(fi * 0.13 + 0.1);
|
|
col += tint * frame * (0.25 + near * 0.75);
|
|
col += tint * exp(-d * 14.0) * near * 0.2;
|
|
|
|
// Beat window — only every third gate's lamp carries the beat,
|
|
// and it is masked by focal proximity so the corridor does not flash as one.
|
|
float gateBeatMask = step(0.68, hash12(vec2(fi * 0.41, floor(u_seed * 0.3)))) * focusField(p) * 0.9 + 0.1;
|
|
float maskedLamps = u_lamps + gateBeatMask * u_beat * 0.45;
|
|
// A lamp at the top of every third gate, pulsing on the beat.
|
|
if (mod(fi, 3.0) < 0.5) {
|
|
vec2 lampAt = vanishAt + vec2(0.0, scale);
|
|
col += pal(4) * exp(-length(p - lampAt) * 26.0) * maskedLamps * (0.3 + near) * (0.6 + 0.4 * gateBeatMask);
|
|
}
|
|
}
|
|
|
|
col = sigAir(col, p, 1.0 - smoothstep(0.0, 1.2, length(q)));
|
|
return vec4(inkValue(col), 1.0);
|
|
}
|
|
`,
|
|
};
|
|
|
|
export default gateCorridor;
|