Phases 8 and 9: shots, and a production design per track

Both phases come out of the manual gate — watching whole tracks — and both
fix something no automated check was looking for.

Phase 8: shots. A section is a STAGE of the song and can run ninety
seconds; one scene held that long reads as a still image with a wobble on
it. Each section kind now gets a roster of three or four stage visuals
instead of one scene, and each section is cut into shots that rotate
between them on phrase lines, never holding past 22s. The roster stays per
kind, so a track's drops still cut between the same images and the video
keeps its identity; the anchor opens each section and the rotation returns
to it, and when a companion is due it is the least recently shown one.

The arc driver stopped working in sections and started working in cues, one
per shot, so a shot cut and a section change take the same code path and
differ only in transition length. The default transition is a slow
dissolve — two bars calm, one loud; a straight cut is reserved for
sections above the energy threshold, because on calm material a cut reads
as a glitch rather than as an edit.

Phase 9: production design. With cuts every fifteen seconds the next
problem was that the images being cut between shared nothing but the
palette. What a music video actually shares across shots is a location, a
cast, a camera operator and an art direction, so each track now generates a
personality in four traits (shape, camera, space, style) off the look seed.
The traits reach shaders as uniforms plus four helpers in the contract, and
each scene expresses them its own way: Classic Wave's rings take the
signature polygon, Metaballs merge as one, Floating Geometry no longer
picks between a box and a circle because the production already decided.

The part that makes it a design rather than a filter: scenes DECLARE which
traits they honour, a track is built on one or two, and a scene that does
not honour all of them is not cast in that track. The library shrinks per
track on purpose.

Two gates keep the declaration honest — lint greps each shader for evidence
of every trait it claims, and a render check measures that each declared
trait actually moves the image (41 scene/trait pairs, weakest response 64
of 255). A layer with no personality renders bit-identically to before,
which is what keeps every earlier sweep and regression valid.

Checks changed rather than added:
  - P4 scene-change and drift checks now measure per shot, not per section;
    the crossfade check reads its length off the cue.
  - P5 flash sweep runs per shot, so the visuals that only appear
    mid-section are measured too.
  - P6 preview/export parity primes first (as both real paths do) and
    compares at the one-LSB tolerance Phase 7 already uses. Measured over
    four consecutive shows: 3 frames at delta 1, then bit-exact — GPU
    variance on first render, not a divergence.
  - P2's contract-uniform list is derived from the contract instead of
    retyped, so the signature uniforms cannot fall out of sync.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dejvino
2026-08-05 20:04:33 +02:00
co-authored by Claude Opus 5
parent 9d15c3cf49
commit ca5a68eb84
39 changed files with 1795 additions and 209 deletions
+25 -3
View File
@@ -15,6 +15,10 @@ export const particleField = {
// legitimately black, so it is judged on variance rather than luminance and
// the look generator only picks it as an accent layer.
role: 'accent',
// Personality: see look/Personality.js. A point cloud cannot draw the
// signature form and has no horizon, so it claims only the camera — which
// it can honour exactly, being the one scene with a real one.
traits: ['camera'],
params: {
count: { type: 'int', range: [200, 4000], default: 1200, bias: 'density', noDrift: true },
@@ -76,7 +80,7 @@ export const particleField = {
return { points, geometry, material, positions, colors, phases, max };
},
update({ instance, camera, timeline, features, params, palette }) {
update({ instance, camera, timeline, features, params, palette, personality }) {
const { geometry, material, positions, colors, phases, max } = instance;
const count = Math.min(max, Math.round(params.count));
const t = timeline.time;
@@ -125,8 +129,26 @@ export const particleField = {
material.size = params.size;
material.opacity = 1;
camera.position.set(0, 0, 4);
camera.lookAt(0, 0, -depth * 0.4);
// The track's camera, applied to the only literal camera in the library:
// the same slow returning pan, sway and roll every shader scene fakes in
// its coordinate space. Bounded and periodic, so a seek still lands on
// the same frame as sequential playback.
const cam = personality ? personality.camera : null;
if (cam) {
const pan = 20 * Math.sin(t * 0.05);
camera.position.set(
Math.cos(cam.driftAngle) * cam.driftRate * pan
+ Math.sin(t * cam.swayRate) * cam.sway,
Math.sin(cam.driftAngle) * cam.driftRate * pan
+ Math.cos(t * cam.swayRate * 0.83) * cam.sway,
4,
);
camera.rotation.z = cam.spin * t;
} else {
camera.position.set(0, 0, 4);
camera.rotation.z = 0;
}
camera.lookAt(camera.position.x, camera.position.y, -depth * 0.4);
},
};
+7 -1
View File
@@ -6,6 +6,8 @@ export const classicWave = {
name: 'Classic Wave',
family: 'flow',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['shape', 'camera', 'style'],
params: {
rings: { type: 'float', range: [4, 40], default: 18, uniform: 'u_rings', bias: 'density' },
@@ -31,7 +33,10 @@ export const classicWave = {
shader: `
vec4 scene(vec2 uv, vec2 p) {
float d = length(p);
p = sigCamera(p);
// The rings take the track's signature form: round tracks get circles,
// hexagonal tracks get hexagonal rings, and it costs one call.
float d = sigShape(p) + 1.0;
float angle = atan(p.y, p.x);
float t = u_time * u_speed + u_seed;
@@ -48,6 +53,7 @@ vec4 scene(vec2 uv, vec2 p) {
// Keep the corners from clipping to flat colour.
col *= 0.6 + 0.4 * (1.0 - smoothstep(0.8, 1.8, d));
col += sigGrain(uv);
return vec4(col, 1.0);
}
@@ -8,6 +8,8 @@ export const curlFlow = {
name: 'Curl Flow',
family: 'flow',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['camera', 'space', 'style'],
params: {
scale: { type: 'float', range: [0.5, 6], default: 2.0, uniform: 'u_scale', bias: 'density' },
@@ -28,6 +30,7 @@ export const curlFlow = {
shader: `
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_speed + u_seed;
p = sigCamera(p);
vec2 flow = curl(p * u_scale + vec2(t, -t * 0.7), t * 0.5);
vec2 q = p + flow * 0.35;
@@ -46,6 +49,8 @@ vec4 scene(vec2 uv, vec2 p) {
col = max(col, trail * u_streak);
col *= 0.6 + 0.4 * exp(-dot(p, p) * 0.35);
col = sigAir(col, p, smoothstep(0.0, 1.7, length(p)));
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
@@ -6,13 +6,16 @@ export const floatingGeometry = {
name: 'Floating Geometry',
family: 'geometric',
kind: 'fragment',
// Personality: see look/Personality.js. This scene is nothing but a handful
// of shapes, so `shape` is the trait it exists to express.
traits: ['shape', 'camera', 'style'],
params: {
count: { type: 'int', range: [2, 14], default: 6, uniform: 'u_count', bias: 'density' },
size: { type: 'float', range: [0.04, 0.3],default: 0.15,uniform: 'u_size' },
drift: { type: 'float', range: [0.1, 1.5], default: 0.4, uniform: 'u_drift', bias: 'motion', rate: true },
spin: { type: 'float', range: [0, 2], default: 0.6, uniform: 'u_spin', rate: true },
boxRatio: { type: 'float', range: [0, 1], default: 0.5, uniform: 'u_boxRatio' },
variety: { type: 'float', range: [0, 0.6], default: 0.25, uniform: 'u_variety' },
aura: { type: 'float', range: [0, 1], default: 0.25,uniform: 'u_aura', bias: 'energy' },
spread: { type: 'float', range: [0.3, 1.2], default: 0.8, uniform: 'u_spread' },
palette: { type: 'palette', count: 5 },
@@ -24,14 +27,9 @@ export const floatingGeometry = {
},
shader: `
float sdCircle(vec2 p, float r) { return length(p) - r; }
float sdBox(vec2 p, vec2 b) {
vec2 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_drift + u_seed;
p = sigCamera(p);
// Background wash from the two darkest palette entries.
vec3 col = mix(pal(0) * 0.18, pal(1) * 0.24, sin(t) * 0.5 + 0.5);
@@ -49,14 +47,21 @@ vec4 scene(vec2 uv, vec2 p) {
vec2 sp = rot(t * (0.2 + fract(s) * u_spin)) * (p - pos);
float size = u_size * (0.6 + fract(s * 0.7) * 0.8);
float d = fract(s * 0.37) < u_boxRatio ? sdBox(sp, vec2(size * 0.8)) : sdCircle(sp, size);
// Every element is the track's signature form. This scene used to pick
// between a box and a circle per element, which is precisely the choice
// the production design should be making — one video, one cast.
// The variety param only scales them apart; it never changes what they are.
float scale = size * (1.0 + (fract(s * 0.37) - 0.5) * u_variety);
float d = sigShape(sp / max(scale, 1e-3)) * scale;
vec3 shapeColor = pal(i + int(floor(t * 0.3)));
float intensity = smoothstep(0.012, 0.0, d);
float intensity = smoothstep(0.012, 0.0, d) + sigEdge(d) * 0.35;
col = mix(col, shapeColor, intensity * 0.85);
col += shapeColor * (1.0 - smoothstep(0.0, size * 2.2, abs(d))) * u_aura;
}
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
@@ -6,6 +6,8 @@ export const horizonLines = {
name: 'Horizon Lines',
family: 'minimal',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['space', 'camera', 'style'],
params: {
count: { type: 'float', range: [3, 40], default: 14, uniform: 'u_count', bias: 'density' },
@@ -25,6 +27,10 @@ export const horizonLines = {
shader: `
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_speed + u_seed;
p = sigCamera(p);
// The bundle sits on the track's horizon rather than in the middle of the
// frame — this scene is nothing but a horizon, so it should be the shared one.
p.y -= sigHorizonY() * 0.6;
// Displace vertically by a slow wave, strongest at the centre of the frame.
float envelope = exp(-p.x * p.x * 1.2);
@@ -41,7 +47,7 @@ vec4 scene(vec2 uv, vec2 p) {
float y = slot + offset * (0.4 + fract(fi * 0.37));
float d = abs(p.y - y);
float line = smoothstep(u_thickness, 0.0, d);
float line = smoothstep(u_thickness * (0.5 + u_sigLine), 0.0, d);
float halo = exp(-d * 26.0) * u_glow;
vec3 c = pal(i);
@@ -51,6 +57,8 @@ vec4 scene(vec2 uv, vec2 p) {
// Keep the far edges dark so the lines read as a subject, not wallpaper.
col *= 0.55 + 0.45 * exp(-dot(p, p) * 0.5);
col = sigAir(col, p, smoothstep(0.0, 1.6, abs(p.y)));
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
+13 -3
View File
@@ -5,6 +5,8 @@ export const kaleidoTunnel = {
name: 'Kaleido Tunnel',
family: 'geometric',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['shape', 'camera', 'style'],
params: {
sides: { type: 'int', range: [2, 12], default: 6, uniform: 'u_sides' },
@@ -25,9 +27,16 @@ export const kaleidoTunnel = {
shader: `
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_speed + u_seed;
p = sigCamera(p);
float radius = max(length(p), 1e-4);
vec2 folded = kaleido(p, float(u_sides));
// A track with a signature polygon dictates the fold count — this is the
// scene where that reads most strongly, so its own sides yields to it.
float sides = u_sigSides > 2.5 ? u_sigSides : float(u_sides);
// Cross-section measured in the signature form: the tunnel mouth is the
// track's shape rather than a circle.
float radius = max(sigShape(p) + 1.0, 1e-4);
vec2 folded = kaleido(p, sides);
float angle = atan(folded.y, folded.x);
// Tunnel coordinates: 1/r is depth, angle is the wall.
@@ -35,7 +44,7 @@ vec4 scene(vec2 uv, vec2 p) {
float wall = angle / 3.14159265 + sin(z * 0.5 + t) * u_twist * 0.25;
float ringLines = abs(fract(z * u_rings * 0.1) - 0.5) * 2.0;
float wallLines = abs(fract(wall * float(u_sides)) - 0.5) * 2.0;
float wallLines = abs(fract(wall * sides) - 0.5) * 2.0;
float grid = smoothstep(0.42, 0.0, ringLines) + smoothstep(0.42, 0.0, wallLines);
@@ -46,6 +55,7 @@ vec4 scene(vec2 uv, vec2 p) {
float fade = smoothstep(0.0, 1.1, radius);
col *= 0.25 + 0.9 * fade;
col += pal(3) * (1.0 - fade) * u_glow * 0.6;
col += sigGrain(uv);
return vec4(col, 1.0);
}
+8 -1
View File
@@ -7,6 +7,8 @@ export const metaballs = {
name: 'Metaballs',
family: 'organic',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['shape', 'camera', 'style'],
params: {
count: { type: 'int', range: [2, 10], default: 5, uniform: 'u_count', bias: 'density' },
@@ -27,6 +29,7 @@ export const metaballs = {
shader: `
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_speed + u_seed;
p = sigCamera(p);
float field = 0.0;
vec3 tint = vec3(0.0);
@@ -41,7 +44,10 @@ vec4 scene(vec2 uv, vec2 p) {
cos(t * (0.5 + fract(s * 0.29)) + s * 1.7) * u_spread * 0.62
);
float d = length(p - centre);
// Distance measured in the track's form rather than as a circle: for a
// round personality this is exactly length(p - centre), and for a
// hexagonal one the blobs merge as hexagons.
float d = sigShape((p - centre) / max(u_radius, 1e-3)) * u_radius + u_radius;
float contribution = (u_radius * u_radius) / max(d * d, 1e-4);
field += contribution;
tint += pal(i) * contribution;
@@ -56,6 +62,7 @@ vec4 scene(vec2 uv, vec2 p) {
vec3 col = pal(0) * 0.06;
col = mix(col, tint, surface);
col += tint * rim * u_rim;
col += sigGrain(uv);
return vec4(col, 1.0);
}
+8 -2
View File
@@ -9,6 +9,8 @@ export const moireGrid = {
name: 'Moiré Grid',
family: 'geometric',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['camera', 'style'],
params: {
density: { type: 'float', range: [6, 60], default: 22, uniform: 'u_density', bias: 'density' },
@@ -42,12 +44,15 @@ float grid(vec2 q, float density, float width) {
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_rotate + u_seed;
p = sigFolded(sigCamera(p));
vec2 warp = vec2(fbm(p * 1.5 + t, 3), fbm(p * 1.5 - t, 3)) - 0.5;
vec2 q = p + warp * u_warp;
float a = grid(rot(t * 6.28318530718) * q, u_density, u_lineWidth);
float b = grid(rot(-t * 6.28318530718 + u_offset * 3.14159) * (q + u_offset), u_density, u_lineWidth);
// Drawn in the track's hand: its line weight scales this scene's.
float weight = u_lineWidth * (0.5 + u_sigLine);
float a = grid(rot(t * 6.28318530718) * q, u_density, weight);
float b = grid(rot(-t * 6.28318530718 + u_offset * 3.14159) * (q + u_offset), u_density, weight);
// The interference term is the point: where both grids land, it peaks.
float interference = a * b;
@@ -59,6 +64,7 @@ vec4 scene(vec2 uv, vec2 p) {
col += pal(3) * pow(interference, 3.0) * u_glow;
col *= 0.6 + 0.4 * exp(-dot(p, p) * 0.3);
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
+8
View File
@@ -8,6 +8,8 @@ export const nebula = {
name: 'Deep Nebula',
family: 'organic',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['camera', 'space', 'style'],
params: {
scale: { type: 'float', range: [4, 24], default: 12, uniform: 'u_scale', bias: 'density' },
@@ -27,6 +29,8 @@ export const nebula = {
shader: `
vec4 scene(vec2 uv, vec2 p) {
// The track's camera films this scene too.
p = sigCamera(p);
float r = length(p);
float a = atan(p.y, p.x);
float t = u_time * u_speed + u_seed;
@@ -51,6 +55,10 @@ vec4 scene(vec2 uv, vec2 p) {
// Vignette the far field so the frame has a subject.
col *= 0.5 + 0.5 * (1.0 - smoothstep(0.6, 1.6, r));
// The location's air, and its surface.
col = sigAir(col, p, smoothstep(0.0, 1.6, r));
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
@@ -6,6 +6,8 @@ export const plasmaBloom = {
name: 'Plasma Bloom',
family: 'organic',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['camera', 'space', 'style'],
params: {
scale: { type: 'float', range: [0.8, 6], default: 2.4, uniform: 'u_scale', bias: 'density' },
@@ -26,6 +28,7 @@ export const plasmaBloom = {
shader: `
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_speed + u_seed;
p = sigCamera(p);
// Two rounds of domain warping. One looks like noise; two looks organic.
vec2 q = vec2(fbm(p * u_scale + t, 4), fbm(p * u_scale + vec2(5.2, 1.3) - t, 4));
@@ -42,6 +45,9 @@ vec4 scene(vec2 uv, vec2 p) {
// Dark corners so the bloom has somewhere to sit.
col *= 0.55 + 0.45 * exp(-dot(p, p) * 0.4);
col = sigAir(col, p, smoothstep(0.0, 1.8, length(p)));
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
@@ -9,6 +9,8 @@ export const psychedelicDrift = {
name: 'Psychedelic Drift',
family: 'glitch',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['shape', 'camera', 'style'],
params: {
count: { type: 'int', range: [4, 24], default: 12, uniform: 'u_count', bias: 'density' },
@@ -66,6 +68,7 @@ vec4 scene(vec2 uv, vec2 p) {
float vs = u_seed + floor(u_variant) * 100.0;
// Swirling background.
p = sigCamera(p);
float bg = sin(p.x * 3.0 + t) * cos(p.y * 3.0 - t * 0.7) + sin(length(p) * 4.0 - t);
vec3 col = mix(pal(0) * 0.12, pal(1) * 0.18, bg * 0.5 + 0.5);
col += pal(0) * 0.05 * beat;
@@ -101,9 +104,11 @@ vec4 scene(vec2 uv, vec2 p) {
float d;
float kind = mod(fi + floor(u_variant), 3.0);
// One of the three symbol kinds is the track's own form, so its cast of
// characters includes the one every other scene is built from.
if (kind == 0.0) d = sdStar(sp, size, 5.0 + floor(hash11(s * 1.2) * 3.0));
else if (kind == 1.0) d = sdSmiley(sp, size, u_time, fi);
else d = sdCircle(sp, size);
else d = sigShape(sp / max(size, 1e-3)) * size;
float hit = 0.0;
for (int j = 0; j < 5; j++) {
@@ -124,6 +129,7 @@ vec4 scene(vec2 uv, vec2 p) {
col += pal(i + 2) * intensity * (0.6 + beat);
}
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
+11 -4
View File
@@ -7,6 +7,8 @@ export const ridgeTerrain = {
name: 'Ridge Terrain',
family: 'structural',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['space', 'camera', 'style'],
params: {
layers: { type: 'int', range: [2, 10], default: 6, uniform: 'u_layers', bias: 'density' },
@@ -27,16 +29,20 @@ export const ridgeTerrain = {
shader: `
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_speed + u_seed;
p = sigCamera(p);
// Its own horizon param decides the framing; the track decides where the
// ground actually is, so two scenes with a horizon agree on one.
float horizon = clamp(u_horizon + sigHorizonY() * 0.35, -0.85, 0.85);
// Sky gradient above the horizon.
float sky = sat((p.y - u_horizon) * 0.8 + 0.5);
float sky = sat((p.y - horizon) * 0.8 + 0.5);
vec3 col = mix(pal(0) * 0.35, pal(1) * 0.18, sky);
// Sparse stars, only in the upper sky, fading as haze rises.
if (u_stars > 0.01 && p.y > u_horizon) {
if (u_stars > 0.01 && p.y > horizon) {
vec2 cell = floor(uv * 220.0);
float rnd = hash12(cell);
float star = step(0.9975, rnd) * sat((p.y - u_horizon) * 2.0);
float star = step(0.9975, rnd) * sat((p.y - horizon) * 2.0);
col += vec3(star) * u_stars * (0.6 + 0.4 * sin(t * 8.0 + rnd * 30.0));
}
@@ -50,7 +56,7 @@ vec4 scene(vec2 uv, vec2 p) {
float x = p.x * mix(0.6, 1.8, depth) + t * parallax + fi * 13.7;
float ridge = fbm(vec2(x, fi * 5.1) * u_rough, 4) - 0.5;
float base = u_horizon - depth * 0.28;
float base = horizon - depth * 0.28;
float top = base + ridge * u_height * mix(0.5, 1.3, depth);
float mask = smoothstep(0.004, 0.0, p.y - top);
@@ -64,6 +70,7 @@ vec4 scene(vec2 uv, vec2 p) {
col += pal(5) * smoothstep(0.02, 0.0, abs(p.y - top)) * (0.12 + depth * 0.25) * u_haze;
}
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
+6 -1
View File
@@ -9,6 +9,8 @@ export const scanTear = {
name: 'Scan Tear',
family: 'glitch',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['style'],
params: {
slices: { type: 'float', range: [4, 48], default: 16, uniform: 'u_slices', bias: 'density' },
@@ -39,7 +41,9 @@ vec4 scene(vec2 uv, vec2 p) {
float rowRandom = hash12(vec2(row, step_));
// Only some rows tear, and only above the tear threshold.
float torn = step(1.0 - u_tear, rowRandom);
// Slice weight follows the track's line weight — the same art direction
// that thickens a grid line thickens a tear.
float torn = step(1.0 - u_tear * (0.6 + u_sigLine), rowRandom);
float offset = (rowRandom - 0.5) * 2.0 * u_shift * torn;
vec2 q = vec2(fract(uv.x + offset), uv.y);
@@ -65,6 +69,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = max(col, ghost * u_persist);
col += pal(4) * torn * u_shift * 0.6;
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
+11 -4
View File
@@ -9,6 +9,8 @@ export const slowOrb = {
name: 'Slow Orb',
family: 'minimal',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['shape', 'camera', 'space', 'style'],
params: {
size: { type: 'float', range: [0.15, 0.8], default: 0.38, uniform: 'u_size', bias: 'energy' },
@@ -29,12 +31,15 @@ export const slowOrb = {
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_drift + u_seed;
vec2 centre = vec2(sin(t * 1.7) * 0.28, cos(t * 1.3) * 0.18);
vec2 q = p - centre;
// The orb drifts about the track's horizon rather than about the middle of
// the frame, so it occupies the same space as the scenes that draw ground.
vec2 centre = vec2(sin(t * 1.7) * 0.28, cos(t * 1.3) * 0.18 + sigHorizonY() * 0.45);
vec2 q = sigCamera(p) - centre;
// Break the silhouette so it never reads as a hard circle.
// The body is the track's signature form — this scene is one shape in an
// empty frame, so the shape had better be the track's.
float wobble = fbm(q * 2.4 + t, 4) * u_wobble;
float d = length(q) * (1.0 + wobble) - u_size;
float d = (sigShape(q / max(u_size, 1e-3)) * u_size) * (1.0 + wobble);
float body = smoothstep(u_softness * 0.5, -u_softness * 0.5, d);
float glow = exp(-max(d, 0.0) * (5.0 / max(u_halo, 0.05))) * u_halo;
@@ -45,7 +50,9 @@ vec4 scene(vec2 uv, vec2 p) {
col += pal(3) * glow * 0.35;
// Fine grain keeps large flat areas from banding.
col = sigAir(col, p, smoothstep(0.0, 1.6, length(q)));
col += (hash12(uv * 640.0 + floor(u_frame)) - 0.5) * u_grain * 0.08;
col += sigGrain(uv);
return vec4(col, 1.0);
}
@@ -9,6 +9,8 @@ export const spectrumSculpture = {
name: 'Spectrum Sculpture',
family: 'minimal',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['shape', 'camera', 'style'],
params: {
bars: { type: 'float', range: [8, 96], default: 40, uniform: 'u_bars', bias: 'density' },
@@ -55,12 +57,15 @@ float bandAt(float x) {
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_rotate + u_seed;
p = sigCamera(p);
float bars = u_bars;
float seg = 6.28318530718 / bars;
float angle = atan(p.y, p.x) + t * seg;
float radius = length(p);
// Radial extent measured in the track's form, so the sculpture is built on
// the same outline as everything else in the video.
float radius = sigShape(p) + 1.0;
float index = floor((angle + 3.14159265) / seg);
float cellAngle = mod(angle + 3.14159265, seg) / seg;
@@ -84,7 +89,8 @@ vec4 scene(vec2 uv, vec2 p) {
col += palRamp(slot * 0.7 + 0.15) * inBar * shape * (0.6 + band);
// Inner ring outline holds the composition together.
col += pal(2) * smoothstep(0.006, 0.0, abs(radius - u_radius)) * 0.35;
col += pal(2) * sigEdge(radius - u_radius) * 0.35;
col += sigGrain(uv);
return vec4(col, 1.0);
}
@@ -10,6 +10,8 @@ export const synthwaveRun = {
name: 'Synthwave Run',
family: 'structural',
kind: 'fragment',
// Personality: see look/Personality.js.
traits: ['space', 'camera', 'style'],
params: {
speed: { type: 'float', range: [0.3, 4], default: 1.5, uniform: 'u_speed', bias: 'motion', rate: true },
@@ -30,7 +32,9 @@ export const synthwaveRun = {
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_speed + u_seed;
float beat = u_beat;
float horizon = u_horizon;
p = sigCamera(p);
// Shares the track's ground line with every other scene that has one.
float horizon = clamp(u_horizon + sigHorizonY() * 0.35, -0.85, 0.85);
vec3 colorMain = pal(0);
vec3 colorSec = pal(1);
@@ -97,6 +101,7 @@ vec4 scene(vec2 uv, vec2 p) {
// Horizon haze, scaled by the glow param.
col += colorMain * exp(-abs(p.y - horizon) * 8.0) * u_glow * 0.35;
col += sigGrain(uv);
return vec4(col, 1.0);
}