music-video-gen/flow-state/src/scenes/shader/quasicrystal.js
Dejvino 44e1826fcb Six more visualizers, built through the scaffolder
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>
2026-08-06 00:14:20 +02:00

78 lines
3.2 KiB
JavaScript

// Geometric family: the interference of several plane waves at irrational
// angles — a pattern with local symmetry that never actually repeats.
//
// Distinct from Moiré Grid (two periodic grids beating against each other),
// Truchet Fold (a tiling, so a cell structure you can see) and Prism Bloom
// (radial geometry around a centre): with five or seven waves the sum is
// quasiperiodic. There is no tile, no cell and no centre, and the eye keeps
// finding order that does not survive being looked at twice. That is a texture
// the library could not previously make.
//
// Scaffolded by tools/new-scene.js. See HOWTO-visualizers.md.
export const quasicrystal = {
name: 'Quasicrystal',
family: 'geometric',
kind: 'fragment',
traits: ['camera', 'style'],
params: {
waves: { type: 'int', range: [3, 11], default: 5, uniform: 'u_waves', bias: 'density' },
frequency: { type: 'float', range: [2, 26], default: 9, uniform: 'u_frequency', bias: 'density' },
speed: { type: 'float', range: [0.02, 0.7], default: 0.15, uniform: 'u_speed', bias: 'motion', rate: true },
contrast: { type: 'float', range: [0.5, 6], default: 2.0, uniform: 'u_contrast' },
threshold: { type: 'float', range: [0, 0.9], default: 0.35, uniform: 'u_threshold' },
glow: { type: 'float', range: [0, 1.5], default: 0.5, uniform: 'u_glow', bias: 'energy' },
breathe: { type: 'float', range: [0, 0.5], default: 0.15, uniform: 'u_breathe' },
palette: { type: 'palette', count: 5 },
},
reactive: {
glow: { feature: 'bandHigh', amount: 0.35, response: 'smooth' },
threshold: { feature: 'bandLow', amount: 0.2, response: 'smooth' },
},
shader: `
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_speed + u_seed;
p = sigFolded(sigCamera(p));
// Sum of N plane waves, each rotated by pi/N. When N is odd the angles are
// incommensurate with any lattice, which is exactly why the result never
// tiles — and why an even N looks disappointingly like a grid.
float sum = 0.0;
float n = max(float(u_waves), 1.0);
float freq = u_frequency * (1.0 + sin(t * 0.5) * u_breathe);
for (int i = 0; i < 11; i++) {
if (i >= u_waves) break;
float a = 3.14159265 * float(i) / n + t * 0.08;
vec2 dir = vec2(cos(a), sin(a));
sum += cos(dot(p, dir) * freq + t * 1.7 + float(i) * 0.6);
}
float field = sum / n;
// Contrast shapes the sum into either soft blobs or hard cells; the
// threshold cuts it into the flat-topped plateaus that read as a lattice of
// rosettes rather than as a ripple.
float shaped = tanh(field * u_contrast);
float plateau = smoothstep(u_threshold - 0.12, u_threshold + 0.12, shaped);
vec3 col = mix(pal(0) * 0.08, palRamp(0.15 + field * 0.35), 0.5 + shaped * 0.5);
col = mix(col, pal(2), plateau * 0.55);
// Ridges: where the sum crosses zero, which is the aperiodic skeleton.
float ridge = 1.0 - abs(shaped);
col += pal(4) * pow(sat(ridge), 6.0) * u_glow;
col += pal(3) * sigEdge(shaped) * u_glow * 0.3;
col *= 0.65 + 0.35 * exp(-dot(p, p) * 0.22);
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
};
export default quasicrystal;