Add Block Mosh, Neon City and Flora visualizers
Three new scene shaders to widen the library, now that the main families are settled. Each is a shader plus a params block, registered in the single source of truth (scenes/registry.js), so casting, UI, arc and checks pick them up automatically. - Block Mosh (glitch): a datamosh. Blocks pull the feedback buffer along per-block strokes on a bar-quantized grid so the corruption steps like an edit rather than crawling, and spills on onsets. Fills the most iconic gap in the glitch family. - Neon City (structural): a receding skyline with instanced lit windows, distinct from the rolling ridgelines and the road grid it sits alongside. - Flora (organic): an abstract plant of swaying stalks, teardrop petals and a bloom corona, all stamped in the track's signature shape. Also adds HOWTO-visualizers.md, a quick reference for building the next one.
This commit is contained in:
@@ -16,6 +16,9 @@ import { kaleidoTunnel } from './shader/kaleido-tunnel.js';
|
||||
import { moireGrid } from './shader/moire-grid.js';
|
||||
import { ridgeTerrain } from './shader/ridge-terrain.js';
|
||||
import { scanTear } from './shader/scan-tear.js';
|
||||
import { blockMosh } from './shader/block-mosh.js';
|
||||
import { neonCity } from './shader/neon-city.js';
|
||||
import { flora } from './shader/flora.js';
|
||||
|
||||
/**
|
||||
* The scene library. Families exist so the arc driver can choose by section
|
||||
@@ -51,6 +54,9 @@ const MODULES = [
|
||||
moireGrid,
|
||||
ridgeTerrain,
|
||||
scanTear,
|
||||
blockMosh,
|
||||
neonCity,
|
||||
flora,
|
||||
];
|
||||
|
||||
const errors = [];
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Glitch family: a datamosh. The frame is cut into a coarse grid of blocks and
|
||||
// each block is pulled along a per-block stroke drawn from the PREVIOUS frame —
|
||||
// so the corruption accrues and slithers across frames instead of tearing once.
|
||||
//
|
||||
// Everything steps on one quantised grid: the block field re-rolls on bar lines
|
||||
// rather than crawling, which is what makes this read as an edit rather than as
|
||||
// a smooth warp. On an onset the spill widens, which is the "hit".
|
||||
|
||||
export const blockMosh = {
|
||||
name: 'Block Mosh',
|
||||
family: 'glitch',
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js.
|
||||
traits: ['camera', 'style'],
|
||||
|
||||
params: {
|
||||
block: { type: 'float', range: [4, 48], default: 22, uniform: 'u_blocks', bias: 'density' },
|
||||
smear: { type: 'float', range: [0, 0.5], default: 0.18, uniform: 'u_smear', bias: 'energy' },
|
||||
quantize: { type: 'float', range: [1, 8], default: 4, uniform: 'u_quantize' },
|
||||
bleed: { type: 'float', range: [0, 1], default: 0.45, uniform: 'u_bleed' },
|
||||
speed: { type: 'float', range: [0.1, 1.4], default: 0.5, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||
burst: { type: 'float', range: [0, 0.4], default: 0.12, uniform: 'u_burst' },
|
||||
palette: { type: 'palette', count: 4 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
smear: { feature: 'bandHigh', amount: 0.3, response: 'smooth' },
|
||||
bleed: { feature: 'flux', amount: 0.2, response: 'spike' },
|
||||
burst: { feature: 'beat', amount: 0.25, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_speed + u_seed;
|
||||
float beat = u_beat;
|
||||
p = 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);
|
||||
base *= 0.35 + 0.55 * sat(field * 1.5);
|
||||
|
||||
// One quantised grid for every jump below, so the mosh steps like an edit
|
||||
// rather than crawling like a smooth warp.
|
||||
float q = max(u_quantize, 1.0);
|
||||
float st = floor(u_barPhase * q * 6.0) + floor(t * 4.0) * (q * 6.0);
|
||||
vec2 cell = floor(uv * u_blocks);
|
||||
|
||||
// Each block gets one fixed stroke per step: a direction and a reach.
|
||||
vec2 stroke = (hash22(cell + st * 31.0) - 0.5) * 2.0;
|
||||
float reach = hash12(cell + st * 17.0);
|
||||
vec2 dst = clamp(uv + stroke * u_smear * (0.3 + reach), 0.0, 1.0);
|
||||
|
||||
// The mosh pulls the PREVIOUS frame along the stroke and layers it over the
|
||||
// fresh field. The previous frame is itself moshed, so smear accrues.
|
||||
vec3 mosh = prev(dst);
|
||||
float m = clamp(u_bleed * (0.5 + reach), 0.0, 1.0);
|
||||
vec3 col = mix(base, mosh, m);
|
||||
|
||||
// Weak block tint, pulse-pinned to the beat but kept small — the frame as a
|
||||
// whole never swings in luminance, so it stays clear of the WCAG 2.3.1
|
||||
// flash ceiling even on a hard four-on-the-floor.
|
||||
float blk = hash12(cell + st * 7.0);
|
||||
col += pal(2) * u_burst * (0.5 + 0.5 * blk) * (0.5 + 0.5 * beat);
|
||||
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default blockMosh;
|
||||
@@ -0,0 +1,119 @@
|
||||
// Organic family: an abstract plant — a plume of swaying stalks with teardrop
|
||||
// leaves and a glowing bloom at each crown.
|
||||
//
|
||||
// Abstract on purpose: no soil, no foreground, just the silhouette of growth.
|
||||
// Every petal and the crown are stamped in the track's signature form, so a
|
||||
// hexagonal track grows hexagonal flora. The stalk bends on bass and the bloom
|
||||
// pulses on the beat — the plant dances to the track rather than the camera
|
||||
// shaking.
|
||||
|
||||
export const flora = {
|
||||
name: 'Flora',
|
||||
family: 'organic',
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js.
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
count: { type: 'float', range: [1, 5], default: 3, uniform: 'u_count', bias: 'density' },
|
||||
height: { type: 'float', range: [0.6, 2.0], default: 1.1, uniform: 'u_height', bias: 'energy' },
|
||||
stem: { type: 'float', range: [0.008, 0.05], default: 0.02, uniform: 'u_stem' },
|
||||
sway: { type: 'float', range: [0.05, 0.5], default: 0.2, uniform: 'u_sway', bias: 'motion' },
|
||||
swayRate:{ type: 'float', range: [0.1, 1.2], default: 0.6, uniform: 'u_swayRate', rate: true },
|
||||
leaf: { type: 'float', range: [0.12, 0.5], default: 0.26, uniform: 'u_leaf' },
|
||||
bud: { type: 'float', range: [0, 1], default: 0.55, uniform: 'u_bud', bias: 'energy' },
|
||||
palette: { type: 'palette', count: 4 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
sway: { feature: 'bandLow', amount: 0.25, response: 'smooth' },
|
||||
bud: { feature: 'beat', amount: 0.3, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
// Horizontal lean of a spine at parametric height h (0 base .. 1 tip), seeded
|
||||
// per plant and swaying more toward the crown like a real stem. Returns a value
|
||||
// scaled by u_sway later, so the reactive bend applies to every part at once.
|
||||
float swayAt(float h, float phase, float t) {
|
||||
return sin(t * (0.35 + fract(phase * 1.7)) + phase)
|
||||
+ h * sin(t * (0.35 + fract(phase * 0.6)) + phase * 2.1);
|
||||
}
|
||||
|
||||
// A petal: an elongated blossom drawn in the track's signature form, growing
|
||||
// forward along dir from a node and clipped to a blade shape.
|
||||
float petal(vec2 p, vec2 node, vec2 dir, float len, float width) {
|
||||
vec2 off = p - node;
|
||||
float along = dot(off, dir);
|
||||
vec2 perp = vec2(-dir.y, dir.x);
|
||||
float across = dot(off, perp);
|
||||
float taper = 0.6 + 0.4 * sat(along / max(len, 1e-3));
|
||||
vec2 q = vec2(along / max(len, 1e-3), across / max(width * taper, 1e-3));
|
||||
float form = sigForm(q * 0.5, vec2(0.0), 0.5);
|
||||
float clip = smoothstep(0.0, -0.1, along) * smoothstep(len + 0.12, len * 0.72, along);
|
||||
return form * clip;
|
||||
}
|
||||
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_swayRate + u_seed;
|
||||
float beat = u_beat;
|
||||
p = sigCamera(p);
|
||||
|
||||
// Soft vertical wash behind the plants.
|
||||
vec3 col = mix(pal(0) * 0.03, pal(1) * 0.06, sat((p.y + 1.0) * 0.5));
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (i >= int(u_count)) break;
|
||||
float fi = float(i);
|
||||
float s = u_seed + fi * 43.7;
|
||||
float baseX = (hash11(s * 1.7) - 0.5) * 1.25;
|
||||
float height = u_height * (0.55 + 0.45 * hash11(s * 3.1));
|
||||
float baseY = -0.92;
|
||||
|
||||
// Stem, sampled so the bend follows the sway curve rather than a chord.
|
||||
float stemMask = 0.0;
|
||||
for (int j = 0; j < 12; j++) {
|
||||
float hj = (float(j) + 0.5) / 12.0;
|
||||
float cx = baseX + swayAt(hj, s, t) * u_sway;
|
||||
float cy = baseY + hj * height;
|
||||
float d = length(p - vec2(cx, cy));
|
||||
float taper = max(u_stem * (1.0 - hj * 0.8) + 0.002, 0.003);
|
||||
stemMask = max(stemMask, smoothstep(taper + 0.004, taper - 0.004, d));
|
||||
}
|
||||
col = mix(col, mix(pal(1), pal(3), 0.3), stemMask);
|
||||
|
||||
// Stalk tip, reused for the petal rows and the bloom.
|
||||
float tipX = baseX + swayAt(1.0, s, t) * u_sway;
|
||||
float tipY = baseY + height;
|
||||
|
||||
// Petals up the upper half, alternating sides.
|
||||
for (int l = 0; l < 6; l++) {
|
||||
float fll = float(l);
|
||||
float node = 0.45 + floor(fll * 0.5) * 0.12;
|
||||
float side = fract(fll * 0.5) < 0.5 ? -1.0 : 1.0;
|
||||
float nx = baseX + swayAt(node, s, t) * u_sway;
|
||||
float ny = baseY + node * height;
|
||||
vec2 dir = normalize(vec2(side, 0.55));
|
||||
float len = u_leaf * 2.0;
|
||||
float m = petal(p, vec2(nx, ny), dir, len, u_leaf);
|
||||
vec3 leafCol = mix(pal(2), pal(3), fract(s * 0.4));
|
||||
col = mix(col, leafCol, m);
|
||||
col += pal(3) * m * (0.2 + 0.3 * beat);
|
||||
}
|
||||
|
||||
// Bloom: a small corona of signature forms around the crown.
|
||||
float bloom = 0.0;
|
||||
for (int k = 0; k < 6; k++) {
|
||||
float a = 6.2831853 * (float(k) + 0.5) / 6.0 + s;
|
||||
vec2 off = vec2(cos(a), sin(a)) * u_leaf * (0.30 + 0.2 * beat);
|
||||
bloom += sigForm(p - vec2(tipX, tipY), off, u_leaf * 0.5);
|
||||
}
|
||||
col = mix(col, pal(2), sat(bloom) * u_bud * (0.6 + 0.4 * beat));
|
||||
}
|
||||
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default flora;
|
||||
@@ -0,0 +1,123 @@
|
||||
// Structural family: a receding skyline of towers with instanced lit windows.
|
||||
//
|
||||
// Distinct from Ridge Terrain (rolling fbm ridgelines) and Synthwave Run (a
|
||||
// ground grid): this is architecture, read as a front-on skyline. Heights and
|
||||
// footprints are per-tower noise, drawn far-to-near so a nearer tower correctly
|
||||
// occludes the one behind while a taller far tower still rises above a short
|
||||
// one in front. Windows are lit per facade cell, so the city has interior life
|
||||
// rather than reading as a lit cardboard cut-out.
|
||||
|
||||
export const neonCity = {
|
||||
name: 'Neon City',
|
||||
family: 'structural',
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js.
|
||||
traits: ['space', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
cells: { type: 'float', range: [0.5, 4], default: 2.0, uniform: 'u_cells', bias: 'density' },
|
||||
layers: { type: 'int', range: [2, 8], default: 6, uniform: 'u_layers', bias: 'density' },
|
||||
height: { type: 'float', range: [0.2, 2.5], default: 1.0, uniform: 'u_height', bias: 'energy' },
|
||||
speed: { type: 'float', range: [0.02, 0.4], default: 0.08, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||
horizon: { type: 'float', range: [-0.3, 0.3], default: 0.0, uniform: 'u_horizon' },
|
||||
haze: { type: 'float', range: [0, 1], default: 0.45, uniform: 'u_haze' },
|
||||
reflect: { type: 'float', range: [0, 1], default: 0.5, uniform: 'u_reflect' },
|
||||
pulse: { type: 'float', range: [0, 1], default: 0.5, uniform: 'u_pulse' },
|
||||
glow: { type: 'float', range: [0, 1.5], default: 0.4, uniform: 'u_glow', bias: 'energy' },
|
||||
palette: { type: 'palette', count: 4 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
height: { feature: 'bandLow', amount: 0.18, response: 'smooth' },
|
||||
pulse: { feature: 'beat', amount: 0.2, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
float towerEdge(vec2 grid, float mullions) {
|
||||
vec2 g = fract(grid);
|
||||
float steel = min(1.0 - smoothstep(0.0, 0.28 * mullions, g.x),
|
||||
1.0 - smoothstep(0.0, 0.28 * mullions, g.y));
|
||||
return steel;
|
||||
}
|
||||
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_speed + u_seed;
|
||||
float beat = u_beat;
|
||||
p = sigCamera(p);
|
||||
// Shares the track's ground line with every other scene that has one.
|
||||
float horizon = clamp(u_horizon + sigHorizonY() * 0.3, -0.9, 0.9);
|
||||
|
||||
// Sky, darkening away from the horizon glow.
|
||||
vec3 sky = mix(pal(3) * 0.06, pal(0) * 0.35,
|
||||
smoothstep(-0.1, 0.7, p.y - horizon));
|
||||
sky *= 0.4 + 0.6 * exp(-abs(p.y - horizon) * 3.0);
|
||||
|
||||
vec3 col = sky;
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (i >= u_layers) break;
|
||||
float fi = float(i);
|
||||
float depth = float(i) / max(float(u_layers - 1), 1.0); // 0 far .. 1 near
|
||||
float parallax = mix(0.25, 1.0, depth);
|
||||
|
||||
// Horizontal column coordinate for this depth ring. Receding layers
|
||||
// get more cells per screen and slide slower, giving parallax.
|
||||
float xsc = mix(1.2, 3.4, depth) * u_cells;
|
||||
float xc = p.x * xsc + t * 0.18 * parallax + fi * 31.7;
|
||||
float cx = floor(xc);
|
||||
float xf = fract(xc);
|
||||
|
||||
float h1 = hash11(cx * 13.1 + fi * 7.3 + u_seed);
|
||||
float h2 = hash11(cx * 61.7 + fi * 5.9 + u_seed * 0.7);
|
||||
|
||||
// Footprint half width, and the tower's top above its base.
|
||||
float footprint = (0.55 + h1 * 0.35) * 0.5; // in cell fracs
|
||||
float inTower = smoothstep(footprint + 0.04, footprint - 0.04,
|
||||
abs(xf - 0.5));
|
||||
|
||||
float baseY = horizon - (0.05 + depth * 0.9);
|
||||
float topY = baseY + u_height * (0.35 + h1 * 1.6) * mix(0.45, 1.0, depth);
|
||||
float body = step(p.y, topY) * step(baseY, p.y);
|
||||
float tower = inTower * body;
|
||||
if (tower < 0.01) continue;
|
||||
|
||||
// Facade, tinted by how far into the screen the ring sits.
|
||||
float gy = clamp((p.y - baseY) / max(topY - baseY, 1e-4), 0.0, 1.0);
|
||||
vec3 facade = mix(pal(1), pal(3), depth);
|
||||
|
||||
// Instanced window grid — each facade cell decides itself lit or dark,
|
||||
// with the pulse raising the lit population and the beat underlining it.
|
||||
vec2 g = vec2(xf * 5.0, gy * 9.0);
|
||||
vec2 cellg = floor(g);
|
||||
float lit = step(0.42, hash12(cellg + fi * 3.1));
|
||||
float steel = 1.0 - min(smoothstep(0.0, 0.35, fract(g.x)),
|
||||
smoothstep(0.0, 0.35, fract(g.y)));
|
||||
vec3 windowCol = mix(pal(2), pal(3), 0.3);
|
||||
vec3 fut = mix(col, windowCol,
|
||||
lit * steel * (0.45 + 0.55 * u_pulse + beat * 0.25));
|
||||
|
||||
// Ground-couple: darker, breath where the tower meets the street.
|
||||
fut *= 0.7 + 0.3 * smoothstep(0.0, 0.5, gy);
|
||||
// Haze pushes the far rings into the sky, the way distance actually does.
|
||||
col = mix(col, fut, tower * (1.0 - u_haze * (1.0 - depth)));
|
||||
}
|
||||
|
||||
// Street: a slate floor with a molten reflection of the nearest towers.
|
||||
if (p.y < horizon) {
|
||||
float street = smoothstep(0.0, -0.6, p.y - horizon);
|
||||
col = mix(col, pal(3) * 0.08, street);
|
||||
float refl = exp(-abs(p.y - horizon) * 4.0) * u_reflect;
|
||||
col += pal(2) * refl * (0.08 + 0.2 * beat) * street;
|
||||
}
|
||||
|
||||
// Horizon haze bloom, the city's glow pooling where towers meet the sky.
|
||||
col += pal(2) * exp(-abs(p.y - horizon) * 9.0) * u_glow;
|
||||
col = sigAir(col, p, smoothstep(0.0, 1.4, length(p)));
|
||||
col += sigGrain(uv);
|
||||
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default neonCity;
|
||||
Reference in New Issue
Block a user