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>
127 lines
5.9 KiB
JavaScript
127 lines
5.9 KiB
JavaScript
// Structural family: an aqueduct marching away across the frame — a lower tier
|
|
// of wide piers, an upper tier of small fast ones, and daylight through every
|
|
// opening.
|
|
//
|
|
// Pylon Grid is open frame: you see through the whole structure and it has no
|
|
// mass. This is masonry. The wall is solid, the light comes through holes cut in
|
|
// it, and the rhythm changes tier to tier — two arches up top for every one
|
|
// below, which is the thing that makes a real aqueduct read as marching rather
|
|
// than repeating. The openings are the track's signature form, so a hexagonal
|
|
// video is an aqueduct of hexagons.
|
|
//
|
|
// No declared slow axis, for the reason Contour Map and Balance Stack give: the
|
|
// candidates were measured — tier height 0.91x, opening size 0.62x, tier count
|
|
// 0.17x — and none of them beats the camera's own drift in Phase 11's
|
|
// ten-second average. The structure is legible because it is high-contrast and
|
|
// mostly still, and that is exactly the kind of image where a slow parameter
|
|
// walk moves less of the frame than a slow pan does.
|
|
|
|
export const aqueductMarch = {
|
|
name: 'Aqueduct March',
|
|
family: 'structural',
|
|
kind: 'fragment',
|
|
texture: 0.9,
|
|
consumes: ['cast', 'ink'],
|
|
traits: ['shape', 'camera', 'space', 'style'],
|
|
|
|
params: {
|
|
bays: { type: 'float', range: [1.2, 6], default: 2.2, uniform: 'u_bays', bias: 'density' },
|
|
opening: { type: 'float', range: [0.18, 0.6], default: 0.42, uniform: 'u_opening' },
|
|
tiers: { type: 'int', range: [1, 4], default: 2, uniform: 'u_tiers' },
|
|
rise: { type: 'float', range: [0.3, 1.3], default: 0.7, uniform: 'u_rise' },
|
|
recede: { type: 'float', range: [0, 0.8], default: 0.35, uniform: 'u_recede' },
|
|
sun: { type: 'float', range: [0, 1.5], default: 0.7, uniform: 'u_sunlight', bias: 'energy' },
|
|
march: { type: 'float', range: [0.005, 0.2], default: 0.04, uniform: 'u_march', bias: 'motion', rate: true },
|
|
palette: { type: 'palette', count: 5 },
|
|
},
|
|
|
|
reactive: {
|
|
sun: { feature: 'loudness', amount: 0.3, response: 'smooth' },
|
|
recede: { feature: 'bandLow', amount: 0.15, response: 'smooth' },
|
|
},
|
|
|
|
shader: `
|
|
// One tier of masonry: a wall with a row of openings cut through it. Returns
|
|
// how much of this pixel is stone.
|
|
float tier(vec2 q, float bays, float top, float bottom, float shift) {
|
|
if (q.y > top || q.y < bottom) return 0.0;
|
|
float h = top - bottom;
|
|
float g = q.x * bays + shift;
|
|
float cx = (fract(g) - 0.5) / bays;
|
|
|
|
// The opening is sized as a FRACTION OF ITS BAY in both directions, not as
|
|
// a circle of some radius: a bay is much wider than it is tall once there
|
|
// are three of them across the frame, and a round hole in it is a porthole
|
|
// rather than an arch. Sized this way the same number reads as the same
|
|
// opening whatever the bay count is.
|
|
float cw = 0.5 / bays; // half a bay across
|
|
float ch = h * 0.4; // half an opening up
|
|
float k = u_opening * 2.4;
|
|
vec2 local = vec2(cx / max(cw * k, 1e-4),
|
|
(q.y - (bottom + h * 0.46)) / max(ch * k, 1e-4));
|
|
|
|
// The opening sits low in the tier, leaving a solid band above it to carry
|
|
// the next tier — which is the whole structural point of an aqueduct.
|
|
float hole = castMain(local) * min(cw, ch) * k;
|
|
return smoothstep(-0.004, 0.004, hole);
|
|
}
|
|
|
|
vec4 scene(vec2 uv, vec2 p) {
|
|
float t = u_time * u_march + u_seed;
|
|
p = sigCamera(p);
|
|
|
|
float horizon = sigHorizonY() * 0.35 - 0.1;
|
|
|
|
// Sky behind the structure, and the ground it stands on.
|
|
vec3 col = mix(pal(1) * 0.22, pal(0) * 0.05, sat((p.y - horizon) * 0.7 + 0.2));
|
|
col += pal(3) * exp(-abs(p.y - horizon) * 5.0) * u_sunlight * 0.5;
|
|
col = mix(col, pal(0) * 0.1, smoothstep(horizon + 0.01, horizon - 0.05, p.y));
|
|
|
|
// Two ranges of the same structure: the far one smaller, slower and hazier,
|
|
// so the aqueduct crosses a valley rather than standing in a diagram.
|
|
// lint: fixed-cost — a near range and a far one
|
|
for (int k = 0; k < 2; k++) {
|
|
float fk = float(k);
|
|
float scale = mix(1.0, 0.35, fk * u_recede * 2.0);
|
|
float lift = horizon + fk * u_recede * 0.25;
|
|
|
|
vec2 q = vec2(p.x / max(scale, 0.15), (p.y - lift) / max(scale, 0.15));
|
|
float shift = t * mix(1.0, 0.35, fk);
|
|
|
|
float stone = 0.0;
|
|
float base = 0.0;
|
|
// lint: fixed-cost — at most four tiers
|
|
for (int i = 0; i < 4; i++) {
|
|
if (i >= u_tiers) break;
|
|
float fi = float(i);
|
|
float bottom = fi * u_rise;
|
|
float top = bottom + u_rise;
|
|
// Each tier up is twice as fine and half as tall a rhythm.
|
|
stone = max(stone, tier(q, u_bays * pow(2.0, fi), top, bottom,
|
|
shift * pow(2.0, fi) + fi * 0.31));
|
|
base = max(base, step(bottom, q.y) * step(q.y, top));
|
|
}
|
|
|
|
// Stone: lit from the sun side, shadowed on the other, courses picked
|
|
// out by a horizontal banding that survives the perspective.
|
|
// Courses are drawn in the track's hand: heavy for a track with a thick
|
|
// line, barely there for a fine one.
|
|
float course = 0.5 + 0.5 * sin(q.y * 60.0);
|
|
course = mix(course, smoothstep(0.35, 0.75, course), u_sigLine);
|
|
vec3 masonry = mix(pal(2) * 0.35, pal(4) * 0.8, sat(q.x * 0.4 + 0.5));
|
|
masonry *= 0.7 + (0.15 + u_sigLine * 0.5) * course;
|
|
masonry += pal(3) * u_sunlight * 0.22 * sat(q.x * 0.5 + 0.5);
|
|
masonry += pal(4) * inkStroke(0.5 - abs(fract(q.x * u_bays) - 0.5) - 0.03) * (0.1 + u_sigLine * 0.9);
|
|
|
|
float hazed = mix(1.0, 0.55, fk * u_recede);
|
|
col = mix(col, masonry * hazed, stone * base);
|
|
}
|
|
|
|
col = sigAir(col, p, smoothstep(-0.2, 0.9, p.y - horizon));
|
|
return vec4(inkValue(col), 1.0);
|
|
}
|
|
`,
|
|
};
|
|
|
|
export default aqueductMarch;
|