Phase 10: variety, twelve scenes, and tooling to write the next one
Watching several finished tracks side by side turned up the problem neither Phase 8 (too few cuts) nor Phase 9 (no through-line) addressed: the same scene cast in two different videos looked like the same footage twice. Section bias is nearly identical between two tracks' drops, so both sampled their parameters around the same centre, and the library's own averageness did the rest. Three answers, none of them a new scene: Temperament — a per-track hand on every parameter dial: intensity, pace, detail, and an extremity that decides how far toward the ends of a range the track is willing to sample. Bias comes from the section and is shared between tracks; temperament comes from the track and is not. Overlays — sometimes a second full scene composited over the shot, from a different family, in a blend that preserves what is underneath and never above 0.6 opacity. Not always: a stack that always doubled up would read as permanently cluttered rather than as occasionally layered. A wider palette — hue now derives from SPECTRAL TILT, the log ratio of treble to body. The centroid is a number most masters sit in the middle of, and the plain body/(body+treble) fraction is worse: low frequencies carry most of the energy in all music, so it read 0.98-1.00 for everything and four different battery tracks came out within 0.02 of each other. The ratio is multiplicative, so its logarithm is what spreads — the same four measure -9.3, -5.0, -4.1, -3.8. Also both ways round the wheel (violet, magenta and pink were unreachable by construction), four new schemes, and seeded chroma profile and lightness curve. Closest battery pair went from 0.005 to 0.113. Twelve scenes take the library to 36, six per family: Aurora Veil, Vortex Drift, Tide Rings, Ink Bleed, Dust Chamber, Salt Flat, Cargo Belt, Gate Corridor, Circuit Bloom, Truchet Fold, Signal Decay, Storm Rift. Weighted toward the 'space' and 'shape' traits, which were thinnest and so the signatures most likely to run a track out of cast — the Phase 9 casting rule means the pool a track draws from is smaller than the library. Also fixes a real one in shots.js: heavy LRU weighting was not enough to make a section reach its whole roster, and a five-shot section still came out 0,2,0,2,0 about a fifth of the time. An unseen companion now wins outright; which one is still free, so only the coverage is guaranteed. Block Mosh declared the camera trait, assigned sigCamera(p) to a p it then never read, and passed the lint's evidence grep. The Phase 9 render gate measured its response to the camera at exactly zero. --- tooling --- Adding a scene was mostly boilerplate and round-trips, which is expensive in both senses. The irreducible cost is the shader body; everything around it is now mechanical: npm run new:scene -- "Name" --family=... --traits=... writes the module, registers it, and leaves a skeleton that already passes every gate, with name-derived constants so two skeletons are not twins. The lint grew the rules that previously needed a GPU to catch: the dead camera above, prev() with no base image, and large loops with no early break (with a `// lint: fixed-cost` opt-out for a genuinely fixed-cost sampling loop). checks.html?scene=Name runs the per-scene acceptance battery for one scene — ten lines and a verdict instead of rendering the whole library to find out whether one shader is alive. The same procedure is a repo skill under .claude/skills/build-visualizer/. --- checks changed, with the measurements --- P5 determinism compared two WebGL CONTEXTS, which is not what it is for. Measured: one context is bit-exact over 40 frames with feedback at 0.6; two contexts disagree by up to 2/255 whether feedback is on or off. It now asserts generation is byte-identical (hard) and rendering within 2/255, since feedback compounds single-level variance. P10's cross-track comparison measures distance RELATIVE to how much image there is. Most scenes are mostly dark, so two genuinely different renders — 25 bars against 53 — scored under 0.02 absolute purely because the black background agrees with itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
// Flow family: curtains of light standing above the track's horizon.
|
||||
//
|
||||
// Distinct from Curl Flow (a full-frame advected field) and Silk Ribbon (one
|
||||
// strand): this is several tall vertical sheets, each rippling on its own phase,
|
||||
// dense at the base and dissolving upward. The rippling is a sum of sines rather
|
||||
// than noise, which is what gives an aurora its folded-sheet look instead of a
|
||||
// smoky one — noise curtains read as fog.
|
||||
//
|
||||
// The horizon is the track's, so this stands in the same place as every other
|
||||
// scene that has ground.
|
||||
|
||||
export const auroraVeil = {
|
||||
name: 'Aurora Veil',
|
||||
family: 'flow',
|
||||
kind: 'fragment',
|
||||
traits: ['camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
curtains: { type: 'int', range: [2, 9], default: 4, uniform: 'u_curtains', bias: 'density' },
|
||||
height: { type: 'float', range: [0.4, 1.8], default: 1.0, uniform: 'u_height' },
|
||||
fold: { type: 'float', range: [0.1, 1.4], default: 0.55, uniform: 'u_fold', bias: 'density' },
|
||||
speed: { type: 'float', range: [0.03, 0.5], default: 0.15, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||
glow: { type: 'float', range: [0, 1.4], default: 0.5, uniform: 'u_glow', bias: 'energy' },
|
||||
ground: { type: 'float', range: [0, 0.8], default: 0.3, uniform: 'u_ground' },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
glow: { feature: 'bandHigh', amount: 0.4, response: 'smooth' },
|
||||
fold: { feature: 'bandLow', amount: 0.3, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_speed + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
float horizon = sigHorizonY() * 0.8 - 0.55;
|
||||
float above = p.y - horizon;
|
||||
|
||||
vec3 col = pal(0) * 0.06;
|
||||
|
||||
for (int i = 0; i < 9; i++) {
|
||||
if (i >= u_curtains) break;
|
||||
float fi = float(i);
|
||||
float s = u_seed + fi * 37.13;
|
||||
|
||||
// Each sheet is a vertical line whose x wanders as a sum of three sines.
|
||||
// Three is the fewest that stops reading as a single wobble.
|
||||
float x = (fract(s * 0.61) - 0.5) * 2.2
|
||||
+ sin(above * 1.7 + t * 1.3 + s) * u_fold * 0.35
|
||||
+ sin(above * 3.1 - t * 0.8 + s * 1.7) * u_fold * 0.18
|
||||
+ sin(above * 0.7 + t * 0.4 + s * 2.3) * u_fold * 0.5;
|
||||
|
||||
float d = abs(p.x - x);
|
||||
float widthAt = 0.05 + above * 0.06 + u_sigLine * 0.05;
|
||||
|
||||
// Bright and tight at the base, wide and faint at the top: the vertical
|
||||
// falloff is what makes it a curtain rather than a stripe.
|
||||
float rise = smoothstep(-0.05, 0.0, above) * exp(-max(above, 0.0) / max(u_height, 0.05));
|
||||
float sheet = exp(-d * d / max(widthAt * widthAt, 1e-5)) * rise;
|
||||
|
||||
vec3 tint = palRamp(fract(s) * 0.5 + above * 0.12 + 0.1);
|
||||
col += tint * sheet * (0.55 + u_glow * 0.6);
|
||||
col += tint * exp(-d * 5.0) * rise * u_glow * 0.12;
|
||||
}
|
||||
|
||||
// Ground: the curtains reflected, dim and compressed.
|
||||
if (u_ground > 0.01 && above < 0.0) {
|
||||
float below = -above;
|
||||
col += pal(2) * u_ground * exp(-below * 6.0) * 0.35;
|
||||
}
|
||||
|
||||
col = sigAir(col, p, smoothstep(0.0, 1.6, abs(p.x)));
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default auroraVeil;
|
||||
@@ -33,13 +33,21 @@ export const blockMosh = {
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_speed + u_seed;
|
||||
float beat = u_beat;
|
||||
p = sigCamera(p);
|
||||
|
||||
// The camera moves the CONTENT, not the block grid. The grid has to stay
|
||||
// screen-aligned or the mosh stops reading as a codec artefact and starts
|
||||
// reading as a moving texture — but the field being moshed is part of the
|
||||
// shot and is filmed by the same operator as every other scene. (Assigning
|
||||
// sigCamera(p) to an otherwise unused p is how this scene originally
|
||||
// "honoured" the trait; the Phase 9 render gate measured the resulting
|
||||
// difference at exactly zero.)
|
||||
vec2 cp = sigCamera(p);
|
||||
|
||||
// The thing being moshed: a sheared banded field, recoloured per track, so
|
||||
// the corruption has real content to drag around even standing alone.
|
||||
float shear = uv.y * (2.5 + u_sigLine * 3.0) + t * 0.45;
|
||||
float field = fbm(vec2(uv.x * 1.2 + sin(shear) * 0.3, uv.y * 2.0 + t * 0.35), 3);
|
||||
vec3 base = palRamp(field * 1.15 + uv.x * 0.2 + t * 0.03);
|
||||
float shear = cp.y * (2.5 + u_sigLine * 3.0) + t * 0.45;
|
||||
float field = fbm(vec2(cp.x * 0.9 + sin(shear) * 0.3, cp.y * 1.5 + t * 0.35), 3);
|
||||
vec3 base = palRamp(field * 1.15 + cp.x * 0.18 + t * 0.03);
|
||||
base *= 0.35 + 0.55 * sat(field * 1.5);
|
||||
|
||||
// One quantised grid for every jump below, so the mosh steps like an edit
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Structural family: horizontal belts of cargo running in alternating
|
||||
// directions, stacked up the frame.
|
||||
//
|
||||
// Distinct from Neon City (a static skyline) and Pylon Grid (perspective depth):
|
||||
// this has no depth at all. It is flat, industrial and lateral — the only scene
|
||||
// in the library whose motion is purely sideways, which is exactly what makes it
|
||||
// cut well against everything that recedes.
|
||||
//
|
||||
// Each crate is stamped in the track's signature form, and the belts step in
|
||||
// bar-quantised lurches rather than sliding, so the movement is mechanical.
|
||||
|
||||
export const cargoBelt = {
|
||||
name: 'Cargo Belt',
|
||||
family: 'structural',
|
||||
kind: 'fragment',
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
belts: { type: 'int', range: [2, 8], default: 4, uniform: 'u_belts', bias: 'density' },
|
||||
crates: { type: 'float', range: [2, 14], default: 6, uniform: 'u_crates', bias: 'density' },
|
||||
crateSize:{ type: 'float', range: [0.15, 0.75],default: 0.42, uniform: 'u_crateSize' },
|
||||
speed: { type: 'float', range: [0.05, 1.0], default: 0.3, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||
gap: { type: 'float', range: [0.02, 0.3], default: 0.1, uniform: 'u_gap' },
|
||||
rails: { type: 'float', range: [0, 1], default: 0.45, uniform: 'u_rails' },
|
||||
lamp: { type: 'float', range: [0, 1.3], default: 0.5, uniform: 'u_lamp', bias: 'energy' },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
lamp: { feature: 'beat', amount: 0.35, response: 'spike' },
|
||||
rails: { feature: 'bandLow', amount: 0.2, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_speed + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
vec3 col = pal(0) * 0.05;
|
||||
|
||||
float span = 2.0 / max(float(u_belts), 1.0);
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (i >= u_belts) break;
|
||||
float fi = float(i);
|
||||
float centre = -1.0 + span * (fi + 0.5);
|
||||
float dy = p.y - centre;
|
||||
if (abs(dy) > span * 0.5) continue;
|
||||
|
||||
float dir = mod(fi, 2.0) < 0.5 ? 1.0 : -1.0;
|
||||
|
||||
// Quantised travel: the belt advances in eighth-bar steps, so cargo
|
||||
// lurches from cell to cell the way a conveyor does.
|
||||
float march = floor((t + fi * 0.37) * 4.0) * 0.25 * dir;
|
||||
float lane = p.x * 0.5 + march;
|
||||
|
||||
float cell = floor(lane * u_crates);
|
||||
float withinCell = fract(lane * u_crates);
|
||||
float rnd = hash12(vec2(cell, fi));
|
||||
|
||||
// Not every cell carries a crate; the gaps are what make it read as
|
||||
// cargo rather than as a stripe pattern.
|
||||
if (rnd > u_gap) {
|
||||
vec2 local = vec2((withinCell - 0.5) * 2.0, dy / max(span * 0.5, 1e-3));
|
||||
float size = u_crateSize * (0.7 + rnd * 0.5);
|
||||
float d = sigShape(local / max(size, 1e-3)) * size;
|
||||
|
||||
vec3 crateColor = pal(int(mod(cell + fi, 4.0)) + 1);
|
||||
col = mix(col, crateColor * (0.35 + rnd * 0.5), smoothstep(0.02, -0.02, d));
|
||||
col += crateColor * sigEdge(d) * (0.4 + u_lamp * 0.5);
|
||||
|
||||
// Lamp on a minority of crates, pulsing on the beat. Local, not
|
||||
// whole-frame: a per-crate blink is not a flash.
|
||||
if (rnd > 0.82) col += pal(4) * smoothstep(0.06, 0.0, length(local)) * u_lamp;
|
||||
}
|
||||
|
||||
// Belt rails, top and bottom of each lane.
|
||||
float rail = smoothstep(0.06, 0.0, abs(abs(dy) - span * 0.45));
|
||||
col += pal(2) * rail * u_rails * 0.5;
|
||||
}
|
||||
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default cargoBelt;
|
||||
@@ -0,0 +1,88 @@
|
||||
// Geometric family: orthogonal traces growing outward from the centre, with a
|
||||
// pad in the track's signature form at every junction.
|
||||
//
|
||||
// Distinct from Moiré Grid (two interfering line grids) and Prism Bloom (folded
|
||||
// radial geometry): the structure here is Manhattan — everything runs at right
|
||||
// angles, and the only curves are the pads. That right-angle language is the
|
||||
// thing the library was missing, and it cuts hard against every radial scene.
|
||||
//
|
||||
// Traces light up in travelling pulses rather than all at once, so the frame is
|
||||
// busy without ever changing brightness as a whole.
|
||||
|
||||
export const circuitBloom = {
|
||||
name: 'Circuit Bloom',
|
||||
family: 'geometric',
|
||||
kind: 'fragment',
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
cells: { type: 'float', range: [2, 14], default: 6, uniform: 'u_cells', bias: 'density' },
|
||||
trace: { type: 'float', range: [0.01, 0.1], default: 0.035,uniform: 'u_trace' },
|
||||
pads: { type: 'float', range: [0, 1], default: 0.55, uniform: 'u_pads' },
|
||||
padSize: { type: 'float', range: [0.04, 0.22],default: 0.1, uniform: 'u_padSize' },
|
||||
pulse: { type: 'float', range: [0, 1.5], default: 0.6, uniform: 'u_pulse', bias: 'energy' },
|
||||
speed: { type: 'float', range: [0.05, 1.2], default: 0.35, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||
fill: { type: 'float', range: [0.2, 0.95], default: 0.6, uniform: 'u_fill', bias: 'density' },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
pulse: { feature: 'bandHigh', amount: 0.4, response: 'smooth' },
|
||||
pads: { feature: 'beat', amount: 0.2, response: 'spike' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_speed + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
vec2 g = p * u_cells;
|
||||
vec2 cell = floor(g);
|
||||
vec2 f = fract(g) - 0.5;
|
||||
|
||||
float rnd = hash12(cell + u_seed);
|
||||
float rnd2 = hash12(cell * 1.7 + 11.3 + u_seed);
|
||||
|
||||
vec3 col = pal(0) * 0.05;
|
||||
|
||||
// Each cell carries a horizontal trace, a vertical one, or both — the L
|
||||
// junctions are what make it read as routing rather than as a grid.
|
||||
float horizontal = step(1.0 - u_fill, rnd);
|
||||
float vertical = step(1.0 - u_fill, rnd2);
|
||||
|
||||
float width = u_trace * (0.6 + u_sigLine);
|
||||
float dH = abs(f.y);
|
||||
float dV = abs(f.x);
|
||||
|
||||
// Distance from the centre of the board, used to gate growth outward.
|
||||
float reach = sat(1.4 - length(p) * 0.5);
|
||||
|
||||
float traceMask = 0.0;
|
||||
if (horizontal > 0.5) traceMask += smoothstep(width, width * 0.35, dH);
|
||||
if (vertical > 0.5) traceMask += smoothstep(width, width * 0.35, dV);
|
||||
traceMask = sat(traceMask) * reach;
|
||||
|
||||
vec3 traceColor = palRamp(rnd * 0.4 + 0.15);
|
||||
col += traceColor * traceMask * 0.5;
|
||||
|
||||
// Travelling pulse: a bright packet running along the trace, its position a
|
||||
// function of the cell's own hash so packets are out of step with each other.
|
||||
float along = horizontal > 0.5 ? f.x : f.y;
|
||||
float packet = fract(rnd * 3.1 + t * (0.4 + rnd2 * 0.8));
|
||||
float dPacket = abs(along - (packet - 0.5));
|
||||
col += pal(4) * traceMask * exp(-dPacket * dPacket * 260.0) * u_pulse;
|
||||
|
||||
// Pads sit where both traces meet, stamped in the signature form.
|
||||
if (horizontal > 0.5 && vertical > 0.5 && rnd2 > 1.0 - u_pads) {
|
||||
float d = sigShape(f / max(u_padSize, 1e-3)) * u_padSize;
|
||||
col += pal(2) * smoothstep(0.01, -0.01, d) * reach * 0.7;
|
||||
col += pal(3) * sigEdge(d) * reach * (0.4 + u_pulse * 0.4);
|
||||
}
|
||||
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default circuitBloom;
|
||||
@@ -0,0 +1,79 @@
|
||||
// Minimal family: a nearly empty volume with a shaft of light through it and a
|
||||
// few motes suspended in the beam.
|
||||
//
|
||||
// Distinct from Firefly Drift (a swarm advected along a flow) and Slow Orb (one
|
||||
// body): almost nothing moves here. The motes hold position and only breathe;
|
||||
// what changes is the light. That makes this one of the very few scenes in the
|
||||
// library that can hold a thirty-second intro without asking for attention.
|
||||
//
|
||||
// The beam lands on the track's horizon, so the room is the same room every
|
||||
// other scene with a floor is standing in.
|
||||
|
||||
export const dustChamber = {
|
||||
name: 'Dust Chamber',
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
traits: ['shape', 'camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
motes: { type: 'int', range: [6, 40], default: 18, uniform: 'u_motes', bias: 'density' },
|
||||
moteSize: { type: 'float', range: [0.004, 0.05], default: 0.014, uniform: 'u_moteSize' },
|
||||
beam: { type: 'float', range: [0.1, 1.0], default: 0.45, uniform: 'u_beam', bias: 'energy' },
|
||||
beamWidth:{ type: 'float', range: [0.15, 1.2], default: 0.5, uniform: 'u_beamWidth' },
|
||||
sway: { type: 'float', range: [0, 0.12], default: 0.04, uniform: 'u_sway' },
|
||||
drift: { type: 'float', range: [0.01, 0.3], default: 0.06, uniform: 'u_drift', bias: 'motion', rate: true },
|
||||
palette: { type: 'palette', count: 4 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
beam: { feature: 'loudness', amount: 0.25, response: 'smooth' },
|
||||
moteSize: { feature: 'beat', amount: 0.2, response: 'spike' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_drift + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
float floorY = sigHorizonY() * 0.7 - 0.6;
|
||||
|
||||
// The shaft: a soft wedge widening as it falls, cut off at the floor.
|
||||
float axis = sin(u_seed * 0.7) * 0.35;
|
||||
float down = sat((1.0 - (p.y - floorY)) * 0.6);
|
||||
float halfWidth = u_beamWidth * (0.25 + down * 0.75);
|
||||
float inBeam = exp(-pow((p.x - axis) / max(halfWidth, 1e-3), 2.0) * 2.2);
|
||||
inBeam *= smoothstep(floorY - 0.05, floorY + 0.5, p.y);
|
||||
|
||||
vec3 col = pal(0) * 0.05;
|
||||
col += pal(1) * inBeam * u_beam * 0.5;
|
||||
|
||||
// The pool where the shaft meets the floor.
|
||||
float pool = exp(-abs(p.y - floorY) * 14.0)
|
||||
* exp(-pow((p.x - axis) / max(halfWidth * 1.3, 1e-3), 2.0));
|
||||
col += pal(2) * pool * u_beam * 0.7;
|
||||
|
||||
// Motes: fixed positions, breathing brightness, only visible in the light.
|
||||
for (int i = 0; i < 40; i++) {
|
||||
if (i >= u_motes) break;
|
||||
float fi = float(i);
|
||||
float s = u_seed + fi * 53.7;
|
||||
|
||||
vec2 at = vec2(
|
||||
(hash11(s) - 0.5) * 2.4 + sin(t * 0.8 + s) * u_sway,
|
||||
(hash11(s + 9.1) - 0.5) * 1.8 + cos(t * 0.6 + s * 1.3) * u_sway
|
||||
);
|
||||
|
||||
float lit = exp(-pow((at.x - axis) / max(halfWidth, 1e-3), 2.0) * 2.0);
|
||||
float pulse = 0.55 + 0.45 * sin(t * 2.0 + s * 3.0);
|
||||
float m = sigForm(p, at, u_moteSize * (0.6 + hash11(s + 3.3)));
|
||||
col += pal(3) * m * lit * pulse * (0.5 + u_beam);
|
||||
}
|
||||
|
||||
col = sigAir(col, p, smoothstep(0.0, 1.5, length(p)));
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default dustChamber;
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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',
|
||||
traits: ['shape', 'camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
gates: { type: 'int', range: [3, 14], default: 8, uniform: 'u_gates', bias: 'density' },
|
||||
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(sigShape(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)));
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default gateCorridor;
|
||||
@@ -0,0 +1,81 @@
|
||||
// Organic family: ink dropped into wet paper, spreading along the fibre.
|
||||
//
|
||||
// The bleed is done with feedback — each frame the previous one is sampled
|
||||
// slightly outward along a noise-warped direction and darkened, which is a
|
||||
// diffusion step in everything but name. Distinct from Curl Flow's trails
|
||||
// (advected along a flow field, so they streak) because this expands in all
|
||||
// directions at once, so it blooms.
|
||||
//
|
||||
// A base field is always drawn, so the scene stands alone before feedback has
|
||||
// converged and survives a seek.
|
||||
|
||||
export const inkBleed = {
|
||||
name: 'Ink Bleed',
|
||||
family: 'organic',
|
||||
kind: 'fragment',
|
||||
traits: ['camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
drops: { type: 'int', range: [1, 6], default: 3, uniform: 'u_drops', bias: 'density' },
|
||||
spread: { type: 'float', range: [0.002, 0.02], default: 0.007, uniform: 'u_spread' },
|
||||
fibre: { type: 'float', range: [0.5, 8], default: 3.0, uniform: 'u_fibre', bias: 'density' },
|
||||
soak: { type: 'float', range: [0.7, 0.99], default: 0.93, uniform: 'u_soak' },
|
||||
density: { type: 'float', range: [0.1, 1.2], default: 0.5, uniform: 'u_density', bias: 'energy' },
|
||||
pace: { type: 'float', range: [0.02, 0.4], default: 0.1, uniform: 'u_pace', bias: 'motion', rate: true },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
density: { feature: 'bandLow', amount: 0.35, response: 'smooth' },
|
||||
fibre: { feature: 'bandAir', amount: 0.2 },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_pace + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
// Paper: a still fibre texture that the ink will follow.
|
||||
float grainField = fbm(p * u_fibre * 2.0 + 17.0, 4);
|
||||
vec3 col = mix(pal(0) * 0.09, pal(1) * 0.14, grainField);
|
||||
|
||||
// Fresh ink. Each drop pulses in and out on its own slow cycle, so the page
|
||||
// is never uniformly saturated and there is always somewhere new bleeding.
|
||||
float ink = 0.0;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (i >= u_drops) break;
|
||||
float fi = float(i);
|
||||
float s = u_seed + fi * 61.3;
|
||||
|
||||
vec2 at = vec2(sin(t * 0.6 + s) * 0.6, cos(t * 0.47 + s * 1.7) * 0.45);
|
||||
float life = 0.5 + 0.5 * sin(t * 1.3 + s * 2.1);
|
||||
float d = length(p - at) + (grainField - 0.5) * 0.15;
|
||||
ink += exp(-d * d * 90.0) * life;
|
||||
}
|
||||
ink = sat(ink) * u_density;
|
||||
|
||||
vec3 inkColor = palRamp(0.45 + grainField * 0.3);
|
||||
col = mix(col, inkColor, ink);
|
||||
|
||||
// The bleed: sample the previous frame outward along the fibre. Reading four
|
||||
// offsets rather than one is what makes it spread in every direction instead
|
||||
// of sliding — one sample is a smear, four is diffusion.
|
||||
vec2 warp = (vec2(fbm(p * u_fibre + 3.0, 3), fbm(p * u_fibre - 7.0, 3)) - 0.5) * 2.0;
|
||||
float r = u_spread;
|
||||
vec3 soaked = (
|
||||
prev(uv + (vec2( 1.0, 0.0) + warp * 0.6) * r) +
|
||||
prev(uv + (vec2(-1.0, 0.0) + warp * 0.6) * r) +
|
||||
prev(uv + (vec2( 0.0, 1.0) + warp * 0.6) * r) +
|
||||
prev(uv + (vec2( 0.0, -1.0) + warp * 0.6) * r)
|
||||
) * 0.25;
|
||||
|
||||
col = max(col, soaked * u_soak);
|
||||
|
||||
col = sigAir(col, p, smoothstep(0.0, 1.7, length(p)));
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default inkBleed;
|
||||
@@ -0,0 +1,83 @@
|
||||
// Minimal family: an empty plain under a very large sky, with one distant form
|
||||
// standing on the horizon.
|
||||
//
|
||||
// The emptiest scene in the library and the most deliberate about it — most of
|
||||
// the frame is a gradient. Distinct from Horizon Lines (a bundle of lines) and
|
||||
// Ridge Terrain (layered silhouettes): there is exactly one object, it is small,
|
||||
// and it is far away. What moves is the light and the heat shimmer.
|
||||
//
|
||||
// The object is the track's signature form, so the thing on the horizon of a
|
||||
// hexagonal video is a hexagon.
|
||||
|
||||
export const saltFlat = {
|
||||
name: 'Salt Flat',
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
traits: ['shape', 'camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
monolith: { type: 'float', range: [0.0, 0.35], default: 0.12, uniform: 'u_monolith' },
|
||||
standing: { type: 'float', range: [-0.7, 0.7], default: 0.0, uniform: 'u_standing' },
|
||||
shimmer: { type: 'float', range: [0, 0.09], default: 0.025,uniform: 'u_shimmer' },
|
||||
glowBand: { type: 'float', range: [0, 1.2], default: 0.45, uniform: 'u_glowBand', bias: 'energy' },
|
||||
ground: { type: 'float', range: [0, 1], default: 0.4, uniform: 'u_ground' },
|
||||
salt: { type: 'float', range: [0, 1], default: 0.35, uniform: 'u_salt', bias: 'density' },
|
||||
pace: { type: 'float', range: [0.02, 0.5], default: 0.12, uniform: 'u_pace', bias: 'motion', rate: true },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
glowBand: { feature: 'loudness', amount: 0.3, response: 'smooth' },
|
||||
shimmer: { feature: 'bandAir', amount: 0.25 },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_pace + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
float horizon = sigHorizonY() * 0.6 - 0.15;
|
||||
|
||||
// Heat shimmer: everything near the horizon wobbles, nothing else does.
|
||||
float nearHorizon = exp(-abs(p.y - horizon) * 5.0);
|
||||
p.x += sin(p.y * 60.0 + t * 6.0) * u_shimmer * nearHorizon;
|
||||
|
||||
float above = p.y - horizon;
|
||||
|
||||
// Sky: a tall gradient, darkest at the top.
|
||||
vec3 col = mix(pal(1) * 0.5, pal(0) * 0.25, sat(above * 0.8 + 0.15));
|
||||
|
||||
// The glow band sitting on the horizon — the light source of the whole scene.
|
||||
col += pal(3) * exp(-abs(above) * 9.0) * u_glowBand * 0.8;
|
||||
|
||||
if (above < 0.0) {
|
||||
float depth = sat(-above * 2.2); // 0 far .. 1 near
|
||||
vec3 plain = mix(pal(2) * 0.5, pal(0) * 0.3, depth);
|
||||
|
||||
// Salt crust: cracked cells, only legible in the near field.
|
||||
vec2 cellUv = vec2(p.x / max(-above * 0.9 + 0.06, 0.02), 1.0 / max(-above + 0.05, 0.02));
|
||||
float crack = abs(fract(cellUv.x * 0.5) - 0.5) + abs(fract(cellUv.y * 0.5) - 0.5);
|
||||
plain += pal(4) * smoothstep(0.42, 0.5, crack) * u_salt * depth * 0.25;
|
||||
|
||||
// Reflection of the glow band, compressed toward the horizon.
|
||||
plain += pal(3) * exp(above * 7.0) * u_glowBand * 0.3;
|
||||
|
||||
col = mix(col, plain, u_ground);
|
||||
}
|
||||
|
||||
// The one object: small, on the horizon, in the track's form.
|
||||
if (u_monolith > 0.005) {
|
||||
vec2 at = vec2(u_standing, horizon + u_monolith * 0.9);
|
||||
float d = sigShape((p - at) / u_monolith) * u_monolith;
|
||||
col = mix(col, pal(0) * 0.12, smoothstep(0.006, -0.006, d));
|
||||
col += pal(4) * sigEdge(d) * (0.3 + u_glowBand * 0.5);
|
||||
}
|
||||
|
||||
col = sigAir(col, p, smoothstep(0.0, 1.4, abs(p.x)));
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default saltFlat;
|
||||
@@ -0,0 +1,90 @@
|
||||
// Glitch family: a stack of oscilloscope traces losing signal.
|
||||
//
|
||||
// Distinct from Scan Tear (rows displaced sideways), Block Mosh (block-level
|
||||
// datamosh) and Pitch Shatter (vertical transposition): nothing here is
|
||||
// displaced at all. The corruption is in the SIGNAL — each trace degrades from a
|
||||
// clean wave into noise as its lock is lost, and regains it. Loss of lock steps
|
||||
// on the bar grid, so traces drop out in time rather than flickering.
|
||||
|
||||
export const signalDecay = {
|
||||
name: 'Signal Decay',
|
||||
family: 'glitch',
|
||||
kind: 'fragment',
|
||||
traits: ['camera', 'style'],
|
||||
|
||||
params: {
|
||||
traces: { type: 'int', range: [2, 10], default: 5, uniform: 'u_traces', bias: 'density' },
|
||||
amplitude:{ type: 'float', range: [0.02, 0.3], default: 0.1, uniform: 'u_amplitude', bias: 'energy' },
|
||||
frequency:{ type: 'float', range: [1, 22], default: 7, uniform: 'u_frequency', bias: 'density' },
|
||||
loss: { type: 'float', range: [0, 0.9], default: 0.35, uniform: 'u_loss' },
|
||||
hiss: { type: 'float', range: [0, 1], default: 0.4, uniform: 'u_hiss' },
|
||||
persist: { type: 'float', range: [0, 0.85], default: 0.4, uniform: 'u_persist' },
|
||||
speed: { type: 'float', range: [0.1, 2.0], default: 0.6, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
amplitude: { feature: 'bandLow', amount: 0.35, response: 'smooth' },
|
||||
hiss: { feature: 'flatness', amount: 0.3 },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_speed + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
// Quantised era: lock is lost and regained on the eighth-bar grid, so
|
||||
// dropouts land with the music instead of crawling.
|
||||
float era = floor(u_barPhase * 8.0) + floor(t * 2.0) * 8.0;
|
||||
|
||||
vec3 col = pal(0) * 0.05;
|
||||
float span = 2.0 / max(float(u_traces), 1.0);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (i >= u_traces) break;
|
||||
float fi = float(i);
|
||||
float centre = -1.0 + span * (fi + 0.5);
|
||||
float s = u_seed + fi * 27.7;
|
||||
|
||||
// How much lock this trace has this era. Below zero it is pure noise.
|
||||
float lock = sat(hash12(vec2(fi, era)) * 1.4 - u_loss);
|
||||
|
||||
// Clean signal: two sines and a slow envelope, so it looks like a
|
||||
// waveform rather than a test tone.
|
||||
float clean = sin(p.x * u_frequency + t * 3.0 + s) * 0.6
|
||||
+ sin(p.x * u_frequency * 2.7 - t * 1.7 + s * 1.3) * 0.4;
|
||||
|
||||
// Noise floor: hashed per pixel column and era, held steady within a
|
||||
// step so it reads as static rather than as a shimmer.
|
||||
float noise = (hash12(vec2(floor(p.x * 220.0), era + fi)) - 0.5) * 2.0;
|
||||
|
||||
float signal = mix(noise, clean, lock);
|
||||
float y = centre + signal * u_amplitude;
|
||||
|
||||
float d = abs(p.y - y);
|
||||
float w = 0.004 + u_sigLine * 0.012;
|
||||
float line = smoothstep(w * 2.5, 0.0, d);
|
||||
|
||||
vec3 tint = palRamp(fract(s) * 0.4 + 0.15);
|
||||
col += tint * line * (0.4 + lock * 0.6);
|
||||
col += tint * exp(-d * 40.0) * 0.25 * lock;
|
||||
|
||||
// Hiss band around an unlocked trace: the visual equivalent of the
|
||||
// sound. Confined to the lane, so it never washes the whole frame.
|
||||
if (lock < 0.4) {
|
||||
float band = exp(-abs(p.y - centre) * 12.0);
|
||||
col += pal(3) * band * abs(noise) * u_hiss * 0.3 * (1.0 - lock);
|
||||
}
|
||||
}
|
||||
|
||||
// Ghost of the previous frame, so a dropout leaves a trail rather than
|
||||
// vanishing cleanly.
|
||||
col = max(col, prev(uv) * u_persist);
|
||||
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default signalDecay;
|
||||
@@ -42,6 +42,8 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
float ph = fk * 2.2 + t * 0.3;
|
||||
float best = 1e9;
|
||||
|
||||
// lint: fixed-cost — this samples the curve at a fixed resolution, so
|
||||
// there is no param to break on. Cost is governed by the 4K budget gate.
|
||||
for (int n = 0; n < 48; n++) {
|
||||
float uu = (float(n) + 0.5) / 48.0;
|
||||
float x = (uu - 0.5) * 2.0 * u_spread;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Glitch family: branching discharge across the sky above the track's horizon.
|
||||
//
|
||||
// The bolt is a recursive-looking zigzag built from stacked hashed segments,
|
||||
// re-struck on the quantised grid rather than continuously, so strikes land on
|
||||
// the music. Between strikes the afterglow decays through feedback, which is
|
||||
// what makes the dark frames read as "after a flash" rather than as empty.
|
||||
//
|
||||
// The flash itself is deliberately LOCAL — the bolt and a halo around it, not
|
||||
// the frame. A full-frame white flash on every kick is exactly the WCAG 2.3.1
|
||||
// failure this library is checked against.
|
||||
|
||||
export const stormRift = {
|
||||
name: 'Storm Rift',
|
||||
family: 'glitch',
|
||||
kind: 'fragment',
|
||||
traits: ['camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
bolts: { type: 'int', range: [1, 5], default: 2, uniform: 'u_bolts', bias: 'density' },
|
||||
jag: { type: 'float', range: [0.05, 0.6], default: 0.25, uniform: 'u_jag' },
|
||||
segments: { type: 'float', range: [4, 20], default: 10, uniform: 'u_segments', bias: 'density' },
|
||||
branch: { type: 'float', range: [0, 1], default: 0.45, uniform: 'u_branch' },
|
||||
afterglow:{ type: 'float', range: [0, 0.9], default: 0.55, uniform: 'u_afterglow' },
|
||||
cloud: { type: 'float', range: [0, 1], default: 0.5, uniform: 'u_cloud' },
|
||||
rate: { type: 'float', range: [0.2, 3.0], default: 1.0, uniform: 'u_rate', bias: 'motion', rate: true },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
branch: { feature: 'bandHigh', amount: 0.3, response: 'smooth' },
|
||||
cloud: { feature: 'bandLow', amount: 0.25, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_rate + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
float horizon = sigHorizonY() * 0.7 - 0.5;
|
||||
|
||||
// Cloud deck above, lit from within.
|
||||
float deck = fbm(vec2(p.x * 1.4 + t * 0.15, p.y * 2.2 - t * 0.05), 5);
|
||||
vec3 col = mix(pal(0) * 0.06, pal(1) * 0.2, deck * u_cloud * smoothstep(horizon, horizon + 1.2, p.y));
|
||||
|
||||
// Strikes step on the eighth-bar grid. Each era re-rolls every bolt.
|
||||
float era = floor(u_barPhase * 8.0) + floor(t) * 8.0;
|
||||
|
||||
for (int b = 0; b < 5; b++) {
|
||||
if (b >= u_bolts) break;
|
||||
float fb = float(b);
|
||||
float boltSeed = hash12(vec2(era, fb * 7.3 + u_seed));
|
||||
|
||||
// Not every bolt fires every era; misfires are what make the ones that
|
||||
// land feel like events.
|
||||
float fires = step(0.35, boltSeed);
|
||||
float age = fract(t * 2.0 + fb * 0.31);
|
||||
float intensity = fires * exp(-age * 6.0);
|
||||
if (intensity < 0.004) continue;
|
||||
|
||||
// The channel: a piecewise-linear zigzag from the cloud deck down to the
|
||||
// horizon, each segment hashed off (era, bolt, segment).
|
||||
float x0 = (hash11(boltSeed * 31.0) - 0.5) * 1.8;
|
||||
float best = 1e3;
|
||||
|
||||
for (int s = 0; s < 20; s++) {
|
||||
if (float(s) >= u_segments) break;
|
||||
float f0 = float(s) / u_segments;
|
||||
float f1 = float(s + 1) / u_segments;
|
||||
|
||||
float yA = mix(1.1, horizon, f0);
|
||||
float yB = mix(1.1, horizon, f1);
|
||||
float xA = x0 + (hash11(boltSeed * 17.0 + float(s) * 3.7) - 0.5) * u_jag * (0.3 + f0);
|
||||
float xB = x0 + (hash11(boltSeed * 17.0 + float(s + 1) * 3.7) - 0.5) * u_jag * (0.3 + f1);
|
||||
|
||||
// Distance to this segment.
|
||||
vec2 a = vec2(xA, yA), bb = vec2(xB, yB);
|
||||
vec2 pa = p - a, ba = bb - a;
|
||||
float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);
|
||||
best = min(best, length(pa - ba * h));
|
||||
|
||||
// Branches: a short spur off some joints, in the same hand.
|
||||
if (hash11(boltSeed * 53.0 + float(s)) < u_branch * 0.4) {
|
||||
vec2 tip = a + vec2((hash11(boltSeed + float(s) * 5.1) - 0.5) * 0.5, -0.12);
|
||||
vec2 pb = p - a, bc = tip - a;
|
||||
float h2 = clamp(dot(pb, bc) / max(dot(bc, bc), 1e-6), 0.0, 1.0);
|
||||
best = min(best, length(pb - bc * h2) + 0.008);
|
||||
}
|
||||
}
|
||||
|
||||
float core = smoothstep(0.012 + u_sigLine * 0.01, 0.0, best);
|
||||
float halo = exp(-best * 12.0);
|
||||
col += pal(4) * core * intensity;
|
||||
col += pal(3) * halo * intensity * 0.5;
|
||||
}
|
||||
|
||||
// Ground catches the light; below the horizon is otherwise near black.
|
||||
if (p.y < horizon) {
|
||||
col *= 0.25;
|
||||
col += pal(2) * exp((p.y - horizon) * 5.0) * 0.15;
|
||||
}
|
||||
|
||||
col = max(col, prev(uv) * u_afterglow);
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default stormRift;
|
||||
@@ -0,0 +1,79 @@
|
||||
// Organic family: interference rings spreading from a handful of drop points,
|
||||
// like rain on still water.
|
||||
//
|
||||
// Distinct from Classic Wave (one centred ring set) and Metaballs (merging
|
||||
// bodies): here several sources overlap and the SUM is what is drawn, so the
|
||||
// image is the interference pattern rather than the rings themselves. Where two
|
||||
// wavefronts meet they cancel, which produces the moving lattice of nodes that
|
||||
// makes water look like water.
|
||||
//
|
||||
// Ring geometry is measured in the track's signature form, so a hexagonal track
|
||||
// gets hexagonal wavefronts.
|
||||
|
||||
export const tideRings = {
|
||||
name: 'Tide Rings',
|
||||
family: 'organic',
|
||||
kind: 'fragment',
|
||||
traits: ['shape', 'camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
sources: { type: 'int', range: [2, 7], default: 4, uniform: 'u_sources', bias: 'density' },
|
||||
wavelength:{ type: 'float', range: [3, 26], default: 10, uniform: 'u_wavelength', bias: 'density' },
|
||||
speed: { type: 'float', range: [0.05, 1.2], default: 0.35, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||
decay: { type: 'float', range: [0.2, 2.0], default: 0.8, uniform: 'u_decay' },
|
||||
caustic: { type: 'float', range: [0, 1.5], default: 0.6, uniform: 'u_caustic', bias: 'energy' },
|
||||
spread: { type: 'float', range: [0.2, 1.1], default: 0.65, uniform: 'u_spread' },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
caustic: { feature: 'beat', amount: 0.3, response: 'smooth' },
|
||||
wavelength: { feature: 'bandMid', amount: 0.2 },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_speed + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
float sum = 0.0;
|
||||
float energy = 0.0;
|
||||
|
||||
for (int i = 0; i < 7; i++) {
|
||||
if (i >= u_sources) break;
|
||||
float fi = float(i);
|
||||
float s = u_seed + fi * 91.7;
|
||||
|
||||
// Sources wander slowly, so the interference lattice never settles into
|
||||
// a fixed pattern the eye can lock onto.
|
||||
vec2 src = vec2(
|
||||
sin(t * 0.21 + s) * u_spread,
|
||||
cos(t * 0.17 + s * 1.4) * u_spread * 0.7
|
||||
);
|
||||
|
||||
// Distance in the signature metric: round tracks get circular wavefronts.
|
||||
float d = sigShape(p - src) + 1.0;
|
||||
float amp = exp(-d * u_decay);
|
||||
sum += sin(d * u_wavelength - t * 6.0 + fract(s) * 6.28) * amp;
|
||||
energy += amp;
|
||||
}
|
||||
|
||||
float wave = sum / max(energy, 1e-3);
|
||||
|
||||
// The node lattice: where the sum passes through zero, the surface is flat
|
||||
// and bright. Squaring the gradient-ish term is what picks those out.
|
||||
float nodes = 1.0 - abs(wave);
|
||||
nodes = pow(sat(nodes), 3.0);
|
||||
|
||||
vec3 col = mix(pal(0) * 0.1, pal(1), sat(wave * 0.5 + 0.5));
|
||||
col += pal(3) * nodes * u_caustic;
|
||||
col += pal(4) * pow(sat(wave), 6.0) * u_caustic * 0.5;
|
||||
|
||||
col = sigAir(col, p, smoothstep(0.0, 1.7, length(p)));
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default tideRings;
|
||||
@@ -0,0 +1,74 @@
|
||||
// Geometric family: a Truchet tiling — every cell carries two quarter-arcs in
|
||||
// one of two rotations, and the arcs join across cell edges into long continuous
|
||||
// curves nobody placed.
|
||||
//
|
||||
// That emergent continuity is the whole point and the reason this does not
|
||||
// duplicate Moiré Grid or Circuit Bloom: the structure is a grid, but the thing
|
||||
// you actually see is a set of wandering closed loops. The tiling re-rolls on
|
||||
// phrase lines rather than crawling, so the maze reconfigures on the music.
|
||||
|
||||
export const truchetFold = {
|
||||
name: 'Truchet Fold',
|
||||
family: 'geometric',
|
||||
kind: 'fragment',
|
||||
traits: ['camera', 'style'],
|
||||
|
||||
params: {
|
||||
cells: { type: 'float', range: [1.5, 12], default: 4, uniform: 'u_cells', bias: 'density' },
|
||||
weight: { type: 'float', range: [0.04, 0.3], default: 0.12, uniform: 'u_weight' },
|
||||
radius: { type: 'float', range: [0.3, 0.7], default: 0.5, uniform: 'u_radius' },
|
||||
churn: { type: 'float', range: [0, 1], default: 0.35, uniform: 'u_churn' },
|
||||
glow: { type: 'float', range: [0, 1.4], default: 0.5, uniform: 'u_glow', bias: 'energy' },
|
||||
drift: { type: 'float', range: [0.0, 0.4], default: 0.08, uniform: 'u_drift', bias: 'motion', rate: true },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
glow: { feature: 'bandMid', amount: 0.35, response: 'smooth' },
|
||||
weight: { feature: 'beat', amount: 0.15, response: 'spike' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_drift + u_seed;
|
||||
p = sigFolded(sigCamera(p));
|
||||
|
||||
// The tiling slides slowly and re-rolls its orientations on the phrase grid.
|
||||
vec2 g = p * u_cells + vec2(t, t * 0.6);
|
||||
vec2 cell = floor(g);
|
||||
vec2 f = fract(g) - 0.5;
|
||||
|
||||
float era = floor(u_phrasePhase * 4.0) * u_churn;
|
||||
float flip = step(0.5, hash12(cell + era + u_seed));
|
||||
|
||||
// Mirror one of the two orientations; the arcs then always meet at edge
|
||||
// midpoints, which is what makes neighbouring cells join up.
|
||||
if (flip > 0.5) f.x = -f.x;
|
||||
|
||||
// Two quarter-arcs, centred on opposite corners.
|
||||
float d1 = abs(length(f - vec2(-0.5, -0.5)) - u_radius);
|
||||
float d2 = abs(length(f - vec2(0.5, 0.5)) - u_radius);
|
||||
float d = min(d1, d2);
|
||||
|
||||
float w = u_weight * (0.5 + u_sigLine);
|
||||
float line = smoothstep(w, w * 0.3, d);
|
||||
float halo = exp(-d * 9.0);
|
||||
|
||||
// Colour by which arc, and by cell, so the continuous curves change hue
|
||||
// along their length rather than being one flat ribbon.
|
||||
float which = d1 < d2 ? 0.0 : 1.0;
|
||||
vec3 tint = palRamp(hash12(cell * 0.7 + era) * 0.5 + which * 0.2 + 0.1);
|
||||
|
||||
vec3 col = pal(0) * 0.05;
|
||||
col += tint * line * 0.75;
|
||||
col += tint * halo * u_glow * 0.35;
|
||||
col += pal(4) * sigEdge(d) * u_glow * 0.25;
|
||||
|
||||
col *= 0.65 + 0.35 * exp(-dot(p, p) * 0.25);
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default truchetFold;
|
||||
@@ -0,0 +1,79 @@
|
||||
// Flow family: spiral arms winding into a slowly wandering core.
|
||||
//
|
||||
// Differential rotation is the point — the inner arms turn faster than the outer
|
||||
// ones, so the arms wind up over time and the image is never twice the same.
|
||||
// Distinct from Curl Flow (isotropic advection with no centre) and Kaleido
|
||||
// Tunnel (rigid radial symmetry receding): this has one centre, real shear, and
|
||||
// no symmetry at all.
|
||||
//
|
||||
// The core is drawn in the track's signature form, so the eye of a hexagonal
|
||||
// track's vortex is a hexagon.
|
||||
|
||||
export const vortexDrift = {
|
||||
name: 'Vortex Drift',
|
||||
family: 'flow',
|
||||
kind: 'fragment',
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
arms: { type: 'int', range: [1, 7], default: 3, uniform: 'u_arms', bias: 'density' },
|
||||
winding: { type: 'float', range: [0.5, 7], default: 2.5, uniform: 'u_winding', bias: 'density' },
|
||||
shear: { type: 'float', range: [0.1, 2.5], default: 1.0, uniform: 'u_shear' },
|
||||
speed: { type: 'float', range: [0.02, 0.6], default: 0.15, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||
turbulence:{ type: 'float', range: [0, 1.2], default: 0.4, uniform: 'u_turbulence' },
|
||||
core: { type: 'float', range: [0.0, 0.4], default: 0.12, uniform: 'u_core', bias: 'energy' },
|
||||
falloff: { type: 'float', range: [0.2, 2.0], default: 0.8, uniform: 'u_falloff' },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
core: { feature: 'beat', amount: 0.3, response: 'spike' },
|
||||
turbulence: { feature: 'bandMid', amount: 0.3, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_speed + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
// The eye wanders, so the composition never sits still even when the arms do.
|
||||
vec2 eye = vec2(sin(t * 0.6) * 0.22, cos(t * 0.47) * 0.16);
|
||||
vec2 q = p - eye;
|
||||
|
||||
float r = max(length(q), 1e-4);
|
||||
float a = atan(q.y, q.x);
|
||||
|
||||
// Differential rotation: angular speed falls off with radius, so the arms
|
||||
// shear. Bounded rather than 1/r, or the core would spin arbitrarily fast.
|
||||
float omega = u_shear / (0.35 + r * 1.6);
|
||||
float wound = a + omega * t * 2.0 + log(r + 0.25) * u_winding;
|
||||
|
||||
// Turbulence breaks the arms into filaments instead of clean spokes.
|
||||
wound += (fbm(q * 3.0 + t * 0.4, 4) - 0.5) * u_turbulence * 2.0;
|
||||
|
||||
float arms = max(float(u_arms), 1.0);
|
||||
float band = sin(wound * arms) * 0.5 + 0.5;
|
||||
band = pow(band, 1.8);
|
||||
|
||||
// Density falls off outward, so the frame has a subject.
|
||||
float envelope = exp(-r * u_falloff);
|
||||
|
||||
vec3 col = pal(0) * 0.06;
|
||||
col += palRamp(wound * 0.08 + r * 0.2) * band * envelope * 0.9;
|
||||
col += pal(4) * pow(band, 4.0) * envelope * 0.5;
|
||||
|
||||
// The eye itself, in the signature form.
|
||||
if (u_core > 0.004) {
|
||||
float d = sigShape(q / u_core) * u_core;
|
||||
col = mix(col, pal(0) * 0.05, smoothstep(0.004, -0.01, d));
|
||||
col += pal(3) * sigEdge(d) * 0.8;
|
||||
col += pal(4) * exp(-max(d, 0.0) * 18.0) * 0.35;
|
||||
}
|
||||
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default vortexDrift;
|
||||
Reference in New Issue
Block a user