Three geometric scenes, and two ways to strobe a whole frame

Voronoi Shatter (cells with no fixed shape, re-cut on the phrase), Apollonian
Gasket (a packing built by inversion, so zooming finds more circles rather than
finer noise) and Isometric Blocks (a lit, gridded, solid surface where Floating
Geometry is bodies adrift).

Two flash failures, both the same mistake in different clothes. Apollonian's hard
depth cutoff popped a whole generation of discs in and out as the fold crossed
it — now faded rather than cut. Isometric Blocks drove block height off the low
end, so every tower in the frame grew and shrank together: 4/s. Moving the
reaction to the edges and to a hash-chosen quarter of the tops keeps the pulse
and drops the frame-wide swing.

Voronoi Shatter declares no slow axis: its re-cut puts the ten-second average
0.127 from itself between any two windows, the highest noise floor in the
library, and no parameter competes with that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dejvino 2026-08-16 23:44:50 +02:00
parent 50ceb7afa1
commit 875c3868dd
4 changed files with 330 additions and 0 deletions

View File

@ -56,6 +56,9 @@ import { suspensionSpan } from './shader/suspension-span.js';
import { stairwellDescent } from './shader/stairwell-descent.js'; import { stairwellDescent } from './shader/stairwell-descent.js';
import { aqueductMarch } from './shader/aqueduct-march.js'; import { aqueductMarch } from './shader/aqueduct-march.js';
import { dataAisle } from './shader/data-aisle.js'; import { dataAisle } from './shader/data-aisle.js';
import { voronoiShatter } from './shader/voronoi-shatter.js';
import { apollonianGasket } from './shader/apollonian-gasket.js';
import { isometricBlocks } from './shader/isometric-blocks.js';
/** /**
* The scene library. Families exist so the arc driver can choose by section * The scene library. Families exist so the arc driver can choose by section
@ -137,6 +140,9 @@ const MODULES = [
stairwellDescent, stairwellDescent,
aqueductMarch, aqueductMarch,
dataAisle, dataAisle,
voronoiShatter,
apollonianGasket,
isometricBlocks,
]; ];
const errors = []; const errors = [];

View File

@ -0,0 +1,103 @@
// Geometric family: an Apollonian packing — a circle filled with circles, each
// gap filled again with smaller ones, forever. Built by inversion rather than by
// drawing, so the nesting is exact at every scale the frame can resolve.
//
// Quasicrystal is the other infinitely-detailed geometric and its detail is
// additive: waves summed until the interference looks complicated. This detail
// is structural — every disc has a definite size and place, and zooming finds
// more of them rather than finer noise. Nothing here is a texture, which is why
// it holds up when the shot pushes in.
export const apollonianGasket = {
name: 'Apollonian Gasket',
family: 'geometric',
kind: 'fragment',
// Drawn geometry; grain only muddies the small discs.
texture: 0.3,
traits: ['camera', 'style'],
params: {
depth: { type: 'int', range: [3, 12], default: 7, uniform: 'u_depth', bias: 'density' },
pack: { type: 'float', range: [0.9, 1.6], default: 1.2, uniform: 'u_pack' },
offset: { type: 'float', range: [0, 0.35], default: 0.2, uniform: 'u_offset' },
zoom: { type: 'float', range: [0.4, 2.2], default: 1.0, uniform: 'u_zoom', slowAxis: true },
rim: { type: 'float', range: [0, 1.5], default: 0.7, uniform: 'u_rim', bias: 'energy' },
churn: { type: 'float', range: [0.004, 0.05], default: 0.015, uniform: 'u_churn', bias: 'motion', rate: true },
palette: { type: 'palette', count: 5 },
},
reactive: {
rim: { feature: 'bandHigh', amount: 0.35, response: 'smooth' },
offset: { feature: 'bandLow', amount: 0.12, response: 'smooth' },
},
shader: `
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_churn + u_seed;
p = sigCamera(p);
p /= max(u_zoom, 0.05);
// The Kleinian fold: reflect into a fundamental domain, then invert through
// the unit circle, over and over. Each pass divides the scale by the amount
// the inversion contracted, and the accumulated scale is what turns the
// whole recursion back into a single distance at the end.
float scale = 1.0;
vec2 q = p;
float touched = 0.0;
// lint: fixed-cost — u_depth inversions, bounded at fourteen
for (int i = 0; i < 14; i++) {
if (i >= u_depth) break;
// Stop when the next generation would be finer than the frame can
// resolve. Without this the deep discs alias into a speckle that reads
// as noise, which is the opposite of what an exact packing is for.
if (scale > 45.0) break;
// Fold into the strip: this is what makes the packing infinite in
// every direction rather than one circle's worth.
q = mod(q + 1.0, 2.0) - 1.0;
q -= vec2(u_offset * sin(t + float(i) * 0.7), u_offset * cos(t * 0.8)) * 0.25;
float r2 = dot(q, q);
float k = u_pack / max(r2, 1e-4);
q *= k;
scale *= k;
touched += 1.0;
}
// Back to a screen-space distance. Without dividing by the accumulated
// scale the small discs would be drawn with the same line weight as the
// large ones and the image would be a solid mat of edges.
float d = (length(q) - 1.0) / scale;
float w = 0.006 + u_sigLine * 0.03;
float edge = smoothstep(w * 2.0, w * 0.3, abs(d));
float glow = exp(-abs(d) * 26.0);
// The deepest generations are held back rather than cut off. A hard depth
// limit makes discs pop in and out as the fold slides across it, and a
// frame-wide population of discs appearing at once measured as a strobe.
float lod = smoothstep(45.0, 18.0, scale);
edge *= mix(0.25, 1.0, lod);
glow *= mix(0.15, 1.0, lod);
// Colour by how deep the point fell before it settled, so the generations
// of the packing are legible as bands rather than all being one hue.
float gen = fract(log2(max(scale, 1e-6)) * 0.15 + 0.5);
vec3 col = pal(0) * 0.06;
col += palRamp(gen) * edge * (0.55 + u_rim * 0.7);
col += palRamp(gen + 0.25) * glow * u_rim * 0.4;
col += pal(4) * sigEdge(d) * 0.5;
// Fill the interiors faintly so the discs are objects and not just outlines.
col += palRamp(gen + 0.5) * smoothstep(0.0, -0.06, d) * 0.12;
col *= 0.7 + 0.3 * exp(-dot(p, p) * 0.2);
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
};
export default apollonianGasket;

View File

@ -0,0 +1,125 @@
// Geometric family: a field of extruded blocks seen isometrically, rising and
// falling on the low end. A solid, lit surface with a light direction — three
// faces per block, and the whole grid reads as a landscape rather than a chart.
//
// Floating Geometry is bodies adrift in space with nothing under them. This is
// the opposite proposition: everything is on one grid, everything touches the
// ground, and the only freedom any block has is its height. The plan of each
// block is the track's signature form, so a hexagonal video gets a honeycomb of
// columns rather than a chessboard of them.
export const isometricBlocks = {
name: 'Isometric Blocks',
family: 'geometric',
kind: 'fragment',
texture: 0.6,
traits: ['shape', 'camera', 'style'],
params: {
grid: { type: 'float', range: [2, 10], default: 4.5, uniform: 'u_grid', bias: 'density', slowAxis: true },
height: { type: 'float', range: [0.1, 0.7], default: 0.35, uniform: 'u_height', bias: 'energy' },
plan: { type: 'float', range: [0.2, 0.5], default: 0.4, uniform: 'u_plan' },
tilt: { type: 'float', range: [0.35, 0.75], default: 0.55, uniform: 'u_isoTilt' },
light: { type: 'float', range: [0, 1.4], default: 0.65, uniform: 'u_light' },
wave: { type: 'float', range: [0, 1], default: 0.5, uniform: 'u_wave' },
roll: { type: 'float', range: [0.01, 0.08], default: 0.035, uniform: 'u_roll', bias: 'motion', rate: true },
palette: { type: 'palette', count: 5 },
},
// The low end deliberately does NOT drive the block heights. Measured at
// aggressive settings that read as 4 flashes a second: every tower in the
// frame grows and shrinks together, which is a whole-frame luminance cycle
// on the kick and exactly what the flash ceiling exists to stop. The beat
// lands on a quarter of the tops instead — same reading, local swing.
reactive: {
light: { feature: 'loudness', amount: 0.25, response: 'smooth' },
},
shader: `
// How tall the block at this cell stands, in screen units. A travelling wave
// plus a fixed per-cell character, so the field has a rhythm running through it
// and a skyline that is still recognisable between beats.
float columnHeight(vec2 cell, float t) {
float own = hash12(cell + u_seed);
float wave = sin(dot(cell, vec2(0.7, 0.5)) - t * 1.1) * 0.5 + 0.5;
// Squared, so most of the field lies flat and the tall ones stand out as
// individual towers. A linear height makes every column tall enough to
// occlude its neighbours, and the field turns into vertical stripes.
return u_height * pow(mix(own, wave, u_wave), 2.4);
}
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_roll + u_seed;
p = sigCamera(p);
float tilt = max(u_isoTilt, 0.1);
float cw = 1.0 / u_grid; // one cell across, on screen
vec3 col = mix(pal(0) * 0.06, pal(1) * 0.14, sat(p.y * 0.4 + 0.5));
// A block standing on row r covers screen rows ABOVE r's ground line, so a
// pixel can only belong to a row at or behind it. Walking a fixed number of
// rows from the back forward and overwriting as we go is the painter's
// algorithm, and it is the whole of the depth sorting an isometric field
// needs — no sorting, no z buffer, just the order of the loop.
float colX = floor(p.x / cw);
float fx = (p.x / cw - colX - 0.5) * cw; // screen offset within the column
// Alternate columns are staggered half a row, which is what interlocks the
// field into a lattice instead of leaving it a set of independent stacks.
float stagger = mod(colX, 2.0) * 0.5;
float rowHere = floor(p.y / (tilt * cw) - stagger);
// Rows FURTHER AWAY stand higher up the frame, so a pixel can only belong
// to a block whose ground line is at or below it — row <= rowHere. Counting
// down from there draws the far ones first and lets the near ones paint
// over them.
// lint: fixed-cost — eight rows of depth, back to front
for (int j = 7; j >= 0; j--) {
float row = rowHere - float(7 - j);
vec2 cell = vec2(colX, row);
float groundY = (row + stagger) * tilt * cw; // where this cell meets the ground
float h = columnHeight(cell, t);
float dy = p.y - groundY; // height up the block face
// Side wall: the plan swept up from the ground line to the top face.
float inX = smoothstep(u_plan * cw, u_plan * cw * 0.85, abs(fx));
float wall = inX * step(0.0, dy) * step(dy, h);
float lit = 0.5 + 0.5 * sat(fx / (u_plan * cw) * 0.8 + 0.5);
// The wall darkens toward the ground, which is the only shading cue
// that tells a tall block from a short one at a glance.
// Neither face takes the loudness. Anything that multiplies a face
// colour multiplies most of the frame, and a field of blocks brightening
// together on the kick measured at 5 flashes a second. The light level
// is expressed on the EDGES, which are a few percent of the image.
vec3 side = mix(pal(0) * 0.5, pal(2) * 0.8, lit) * 0.75;
side *= 0.45 + 0.55 * sat(dy / max(h, 1e-3));
col = mix(col, side, wall);
// Top face: the track's signature form, lying flat, lifted by h and
// squashed by the tilt — so every block in the field is the same
// silhouette as every other subject in the video.
vec2 q = vec2(fx, (p.y - groundY - h) / tilt);
float dTop = sigShape(q / (u_plan * cw)) * (u_plan * cw);
float topMask = smoothstep(0.004, -0.004, dTop);
vec3 top = mix(pal(2), pal(3), sat(h / max(u_height, 1e-3))) * 0.8;
col = mix(col, top, topMask);
// A quarter of the blocks answer the beat, chosen by cell rather than
// by anything global, so the field has a pulse without the frame having
// a flash.
float picked = step(0.75, hash12(cell * 3.17 + 5.0));
col += pal(3) * topMask * picked * u_beat * 0.45;
col += pal(4) * sigEdge(dTop) * (0.2 + u_light * 1.2) * step(0.0, dy);
}
col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.3);
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
};
export default isometricBlocks;

View File

@ -0,0 +1,96 @@
// Geometric family: a Voronoi tessellation — every point in the frame belongs
// to whichever seed is nearest, so the plates are irregular, they meet in clean
// straight seams, and they cannot overlap or leave a gap.
//
// Truchet Fold is the other tiling here and it is a regular grid wearing a
// pattern: the cells are square whatever is drawn in them. These cells have no
// fixed shape at all — each one is whatever its neighbours leave it — so when
// the seeds move, every plate in the frame changes shape at once and the seams
// slide along themselves. The seeds jump to new positions on the phrase, which
// re-cuts the whole plane in one frame without anything sliding.
//
// No declared slow axis. The re-cut is the point of the scene and it puts the
// ten-second average 0.127 away from itself between any two windows — the
// highest noise floor of anything in the library — so every candidate axis
// measured at 0.03x to 0.1x. Nothing a parameter can do competes with the plane
// being re-cut, which is the honest description of the scene.
export const voronoiShatter = {
name: 'Voronoi Shatter',
family: 'geometric',
kind: 'fragment',
texture: 0.5,
traits: ['camera', 'style'],
params: {
cells: { type: 'float', range: [1.2, 8], default: 3.0, uniform: 'u_cells', bias: 'density' },
jitter: { type: 'float', range: [0, 1], default: 0.75, uniform: 'u_jitter' },
seam: { type: 'float', range: [0.01, 0.14], default: 0.05, uniform: 'u_seam' },
recut: { type: 'float', range: [0, 1], default: 0.4, uniform: 'u_recut' },
relief: { type: 'float', range: [0, 1], default: 0.5, uniform: 'u_relief' },
glow: { type: 'float', range: [0, 1.5], default: 0.6, uniform: 'u_glow', bias: 'energy' },
drift: { type: 'float', range: [0.01, 0.4], default: 0.09, uniform: 'u_drift', bias: 'motion', rate: true },
palette: { type: 'palette', count: 5 },
},
reactive: {
glow: { feature: 'bandMid', amount: 0.35, response: 'smooth' },
jitter: { feature: 'bandLow', amount: 0.2, response: 'smooth' },
},
shader: `
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_drift + u_seed;
p = sigCamera(p);
vec2 g = p * u_cells;
vec2 base = floor(g);
vec2 f = g - base;
// The plane is re-cut on the phrase: the seeds do not travel to their new
// positions, they are simply somewhere else on the next frame. A tiling
// that slides is a texture; a tiling that jumps is an event.
float era = floor(u_phrasePhase * 2.0) * u_recut;
// Nearest and second-nearest seed. The difference between the two is the
// distance to the seam, which is what makes the borders even in width
// instead of pinching at the corners.
float d1 = 8.0, d2 = 8.0;
vec2 id1 = vec2(0.0);
// lint: fixed-cost — the 3x3 neighbourhood a Voronoi cell can reach
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
vec2 cell = base + vec2(float(i), float(j));
vec2 h = hash22(cell + era * 7.3 + u_seed);
vec2 seed = vec2(float(i), float(j)) +
mix(vec2(0.5), h + 0.15 * sin(t * 2.0 + h * 6.2831), u_jitter);
float d = length(f - seed);
if (d < d1) { d2 = d1; d1 = d; id1 = cell; }
else if (d < d2) { d2 = d; }
}
}
float border = (d2 - d1) / u_cells; // scene units to the seam
// Plates: each one flat, tinted by its own identity, and tilted a little so
// the tessellation reads as broken glass rather than as a colour chart.
float who = hash12(id1 * 1.7 + era);
vec3 plate = palRamp(0.15 + who * 0.5);
float tilt = mix(1.0, 0.55 + who * 0.9, u_relief);
vec3 col = plate * 0.28 * tilt;
col += plate * exp(-d1 * 2.5) * u_glow * 0.25;
// Seams: drawn in the track's hand, brightest where three plates meet.
float seam = smoothstep(u_seam, u_seam * 0.15, border);
col += pal(4) * seam * (0.4 + u_glow * 0.8);
col += pal(3) * sigEdge(border - u_seam) * 0.4;
col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.25);
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
};
export default voronoiShatter;