music-video-gen/flow-state/src/scenes/shader/gate-corridor.js
Dejvino 89e05459c0 Every shot stands on something, and the frame has two ends
A section used to be one scene, and two thirds of the library is composable —
sparse by design, elements ON something. Cast as backgrounds anyway, they left
9 of 40 sampled frames under 20% painted, the darkest at 0.3%: a minute and a
half of a few bright things on black, invisible to every gate because every
gate on the stack was a limit rather than a floor.

Every section now stands on a GROUND: a canvas that fills the frame, cast per
section kind so a shot cut changes the shot and not the world. When the shot
fills the frame itself it IS the ground — two canvases stacked is two pictures
fighting. Above that, a coverage BUDGET: director appetite times the section's
energy times where the story is, capped at two frames' worth of material.

The measured facts move into the repo. scenes/metadata.json is generated from
the gallery — coverage as a shot, coverage as a bed, variety, the structural
profile — tracked in git, stamped with a fingerprint of the scenes and the
metric definitions, and refreshed from gallery.html. `surface` is derived from
it rather than declared; nine scenes claimed `canvas` while painting under a
third of the frame, and declaring it is now a lint error. The generator weights
every layering choice by measured structural distance, because family labels
and the render disagree: two `geometric` scenes can be 0.31 apart and a `flow`
and an `organic` scene 0.04.

The gallery's 0.1 red line is gone. It was right when a section was one scene
and wrong now — nineteen scenes were failing a bar for being consistent, which
is a virtue in an ingredient.

Chasing the numbers turned up four real faults:

  * A scene that reads prev() cannot be a ground. It returns the whole
    composited frame including the layers above it, so a datamosh under a shot
    is eating it: the render stopped reproducing from a seek and two WebGL
    contexts diverged by 91/255 against a tolerance of 4.
  * Screen was the wrong operator for a shot over a bed. It lightens, so a
    median quarter of every frame clipped to paper and whole sections rendered
    100% white. Replaced by a lumakey — the shot's brightness is its alpha.
  * Feedback was an accumulator: a still image settled at 2.3x its own
    brightness. Fine over black, fatal over a filled ground. Normalised at 0.6,
    plus a highlight shoulder so the top rolls off instead of clipping.
  * useTrack never prewarmed, so a fresh Show's first frame differed from every
    later render of it — the export-breaking hazard Compositor.prime documents.

Blazing is a decision now, not a side effect: directors declare an appetite for
it, a section must be loud and late in the story to earn one, and quiet kinds
never do. The ceiling gate matches that — a hard cap per section, and no more
than a fifth of them hot at all.

Rendered across twelve videos, middle of every section:

    painted        51% mean, darkest 0.3%  ->  87% mean, darkest 43%
    clipped white  24% median, worst 100%  ->   1% mean, worst 30%
    separation     0.10                    ->  0.44

Seven scenes can ground a section — five geometric, two organic — so every
quiet section of every video stands on one of two beds. That is the library's
largest hole and it is scene work: there is no minimal or flow canvas that
fills half the frame without reading prev().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 09:02:01 +02:00

92 lines
3.7 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.35, response: 'spike' },
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;
// 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) * u_lamps * (0.3 + near);
}
}
col = sigAir(col, p, 1.0 - smoothstep(0.0, 1.2, length(q)));
return vec4(inkValue(col), 1.0);
}
`,
};
export default gateCorridor;