Seven per family now, 42 scenes. Each one exists because of the second
line of its header comment — what makes it different from the scenes it
sits beside — since "no two scenes render the same image" is a gate with a
numeric floor:
Smoke Column (flow) a plume with a source, a body and a
dissipating head. Everything moves one way and
widens; an isotropic field has no up.
Cell Divide (organic) a partition, not objects. Every pixel belongs
to a cell, boundaries are hard, no background.
Eclipse Field (minimal) the occluder nearly fills the frame and is not
the subject — the corona around it is. The
library's one high-contrast minimal scene.
Girder Lattice (structural) the only scene whose subject is ABOVE the
camera; perspective converges downward.
Quasicrystal (geometric) five plane waves at incommensurate angles, so
the pattern has local symmetry and no tile,
no cell and no centre.
Time Smear (glitch) nothing is displaced. Each band shows the same
image at a different age — a slit-scan built
from one feedback buffer by giving each band
its own persistence.
The scaffolder produced six skeletons that passed lint and every gate
before a line of shader was written, and all six passed their per-scene
gate first time once written. That is what it was for.
Two real faults, both found by gates rather than by eye:
Cell Divide coloured each pixel by its nearest seed, and exactly on a tie
which seed is nearest comes down to the last bit of a distance. Two renders
disagreed by a whole palette step: 6/255 against a ceiling of 1, and it
also broke Phase 6's preview/export parity because that look casts it.
Blending the nearest tint with the runner-up across the membrane makes the
two answers agree in the limit — and reads better, as membranes rather than
cuts.
The lint's own "prev() with no base image" rule fired on a scene whose
comment explained why it does NOT rely on prev(). Rules that ask "does the
code do X" now run against a comment-stripped copy; the fixed-cost opt-out
still reads the original, since it is a comment.
Phase 6's parity tolerance goes from 1/255 to 2. Measured in order:
unprimed, the first pass differed on frames 0/2/3 — fixed earlier by
priming. Primed and isolated: 0/40 at delta 0, three times over. Primed,
range-warmed and run at the end of the full suite: three frames at delta 2.
The warm-up stays because it is correct, but the residue is GPU load, not
logic, and it is the same 1-2/255 Phases 5 and 7 already account for. A
real divergence scores in the tens.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
4.9 KiB
JavaScript
126 lines
4.9 KiB
JavaScript
// Structural family: an overhead truss, seen from underneath.
|
|
//
|
|
// Distinct from Pylon Grid (columns standing on the ground), Gate Corridor
|
|
// (frames receding head-on) and Neon City (a skyline at eye level): this is the
|
|
// only scene in the library whose subject is ABOVE the camera. Two parallel
|
|
// chords run away toward the horizon with diagonal bracing zigzagging between
|
|
// them, and the whole thing is looked up at, so the perspective converges
|
|
// downward rather than inward.
|
|
//
|
|
// Joint plates are stamped in the track's signature form.
|
|
//
|
|
// Scaffolded by tools/new-scene.js. See HOWTO-visualizers.md.
|
|
|
|
export const girderLattice = {
|
|
name: 'Girder Lattice',
|
|
family: 'structural',
|
|
kind: 'fragment',
|
|
traits: ['shape', 'camera', 'space', 'style'],
|
|
|
|
params: {
|
|
bays: { type: 'int', range: [3, 16], default: 8, uniform: 'u_bays', bias: 'density' },
|
|
beam: { type: 'float', range: [0.01, 0.12],default: 0.04, uniform: 'u_beam' },
|
|
depth: { type: 'float', range: [0.3, 2.2], default: 1.0, uniform: 'u_depth' },
|
|
brace: { type: 'float', range: [0, 1], default: 0.7, uniform: 'u_brace', bias: 'density' },
|
|
travel: { type: 'float', range: [0.0, 0.6], default: 0.12, uniform: 'u_travel', bias: 'motion', rate: true },
|
|
plate: { type: 'float', range: [0, 0.12], default: 0.05, uniform: 'u_plate' },
|
|
lamp: { type: 'float', range: [0, 1.4], default: 0.45, uniform: 'u_lamp', bias: 'energy' },
|
|
palette: { type: 'palette', count: 5 },
|
|
},
|
|
|
|
reactive: {
|
|
lamp: { feature: 'beat', amount: 0.3, response: 'spike' },
|
|
brace: { feature: 'bandLow', amount: 0.15, response: 'smooth' },
|
|
},
|
|
|
|
shader: `
|
|
// Distance to a line segment. The truss is nothing but segments.
|
|
float segment(vec2 p, vec2 a, vec2 b) {
|
|
vec2 pa = p - a, ba = b - a;
|
|
float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);
|
|
return length(pa - ba * h);
|
|
}
|
|
|
|
vec4 scene(vec2 uv, vec2 p) {
|
|
float t = u_time * u_travel + u_seed;
|
|
p = sigCamera(p);
|
|
|
|
// Looking up: the vanishing point sits BELOW the frame's horizon, and the
|
|
// structure spans above it. Everything converges toward that point.
|
|
float vanishY = sigHorizonY() * 0.5 - 0.35;
|
|
float above = p.y - vanishY;
|
|
|
|
// Sky between the members.
|
|
vec3 col = mix(pal(0) * 0.1, pal(1) * 0.06, sat(above * 0.6));
|
|
|
|
if (above <= 0.001) {
|
|
col = sigAir(col, p, 1.0);
|
|
col += sigGrain(uv);
|
|
return vec4(col, 1.0);
|
|
}
|
|
|
|
float best = 1e3;
|
|
float nearest = 0.0;
|
|
float plateHit = 1e3;
|
|
|
|
// Bays march toward the camera on a wrapping saw, so the truss travels
|
|
// overhead rather than sitting still.
|
|
for (int i = 0; i < 16; i++) {
|
|
if (i >= u_bays) break;
|
|
float fi = float(i);
|
|
float phase = fract(fi / float(u_bays) + t);
|
|
|
|
// Perspective: nearer bays are further up the frame and further apart.
|
|
float y0 = vanishY + exp(phase * 2.4) * 0.12 * u_depth;
|
|
float y1 = vanishY + exp((phase + 1.0 / float(u_bays)) * 2.4) * 0.12 * u_depth;
|
|
float halfW0 = (y0 - vanishY) * 1.5;
|
|
float halfW1 = (y1 - vanishY) * 1.5;
|
|
|
|
// The two chords, running away from the camera.
|
|
best = min(best, segment(p, vec2(-halfW0, y0), vec2(-halfW1, y1)));
|
|
best = min(best, segment(p, vec2(halfW0, y0), vec2(halfW1, y1)));
|
|
|
|
// Cross member at this joint.
|
|
best = min(best, segment(p, vec2(-halfW0, y0), vec2(halfW0, y0)));
|
|
|
|
// Diagonal bracing, alternating direction bay to bay: the zigzag is
|
|
// what makes it a truss rather than a ladder.
|
|
if (u_brace > 0.05) {
|
|
float flip = mod(fi, 2.0) < 0.5 ? 1.0 : -1.0;
|
|
float d = segment(p, vec2(-halfW0 * flip, y0), vec2(halfW1 * flip, y1));
|
|
best = min(best, d + (1.0 - u_brace) * 0.05);
|
|
}
|
|
|
|
if (u_plate > 0.004) {
|
|
float pd = sigShape((p - vec2(halfW0, y0)) / u_plate) * u_plate;
|
|
plateHit = min(plateHit, pd);
|
|
pd = sigShape((p - vec2(-halfW0, y0)) / u_plate) * u_plate;
|
|
plateHit = min(plateHit, pd);
|
|
}
|
|
|
|
if (phase > 0.75) nearest = max(nearest, phase);
|
|
}
|
|
|
|
// Members are drawn thicker nearer the camera, which is the only depth cue
|
|
// a wireframe has.
|
|
float weight = u_beam * (0.4 + above * 0.9) * (0.5 + u_sigLine);
|
|
float steel = smoothstep(weight, weight * 0.3, best);
|
|
|
|
col = mix(col, pal(2) * (0.3 + above * 0.5), steel);
|
|
col += pal(3) * sigEdge(best - weight) * 0.4;
|
|
|
|
// Joint plates.
|
|
col = mix(col, pal(4) * 0.7, smoothstep(0.006, -0.006, plateHit));
|
|
|
|
// A lamp slung under the nearest bay, pulsing on the beat. Local.
|
|
col += pal(4) * exp(-length(p - vec2(0.0, vanishY + nearest * 1.4)) * 14.0) * u_lamp;
|
|
|
|
col = sigAir(col, p, 1.0 - smoothstep(0.0, 1.4, above));
|
|
col += sigGrain(uv);
|
|
return vec4(col, 1.0);
|
|
}
|
|
`,
|
|
};
|
|
|
|
export default girderLattice;
|