Phase 2: param schema, auto-binding, generated UI
All five party-stage shaders now ported. Each declares its params declaratively; uniform binding, UI controls, seeded sampling and arc automation all derive from that one block, so a new scene costs a shader and a schema and nothing else. Port changes: LED-grid masks stripped, hardcoded colours replaced with palette lookups, magic numbers lifted into params. Psychedelic Drift's internal 15-second scene timer removed — keeping the visuals moving is the arc driver's job, and it knows where the song's real transitions are. ParamPanel generates controls from the schema alone and knows about no specific scene; a hand-written control would be a bug. Gate 8/8: schemas valid, uniforms accounted for in both directions, all scenes compile and render, 128 range-sweep frames with no black, blown or flat results, every param exposed in the UI, edits clamped, presets round-trip and survive schema drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { validateModule } from '../params/schema.js';
|
||||
|
||||
import { nebula } from './shader/nebula.js';
|
||||
import { classicWave } from './shader/classic-wave.js';
|
||||
import { floatingGeometry } from './shader/floating-geometry.js';
|
||||
import { synthwaveRun } from './shader/synthwave-run.js';
|
||||
import { psychedelicDrift } from './shader/psychedelic-drift.js';
|
||||
|
||||
/**
|
||||
* The scene library. Families exist so the arc driver can choose by section
|
||||
@@ -18,6 +22,10 @@ export const FAMILIES = {
|
||||
|
||||
const MODULES = [
|
||||
nebula,
|
||||
classicWave,
|
||||
floatingGeometry,
|
||||
synthwaveRun,
|
||||
psychedelicDrift,
|
||||
];
|
||||
|
||||
const errors = [];
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Ported from party-stage's "Classic Wave". The original picked a hue from a
|
||||
// rolling HSV ramp; here the ramp indexes the palette instead, so the look
|
||||
// generator can actually recolour it.
|
||||
|
||||
export const classicWave = {
|
||||
name: 'Classic Wave',
|
||||
family: 'flow',
|
||||
kind: 'fragment',
|
||||
|
||||
params: {
|
||||
rings: { type: 'float', range: [4, 40], default: 18, uniform: 'u_rings', bias: 'density' },
|
||||
spokes: { type: 'int', range: [0, 12], default: 5, uniform: 'u_spokes' },
|
||||
speed: { type: 'float', range: [0.2, 2.5], default: 1.0, uniform: 'u_speed', bias: 'motion' },
|
||||
colorRoll: { type: 'float', range: [0, 0.5], default: 0.1, uniform: 'u_colorRoll' },
|
||||
softness: { type: 'float', range: [0, 1], default: 0.4, uniform: 'u_softness' },
|
||||
bloomCore: { type: 'float', range: [0, 1.5], default: 0.5, uniform: 'u_bloomCore', bias: 'energy' },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
bloomCore: { feature: 'beat', amount: 0.4, response: 'spike' },
|
||||
rings: { feature: 'bandMid', amount: 0.12 },
|
||||
speed: { feature: 'loudness', amount: 0.15, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float d = length(p);
|
||||
float angle = atan(p.y, p.x);
|
||||
float t = u_time * u_speed + u_seed;
|
||||
|
||||
float wave = sin(d * u_rings - t * 2.0);
|
||||
float spoke = u_spokes > 0 ? sin(angle * float(u_spokes) + t) * u_beat : 0.0;
|
||||
|
||||
float v = 0.5 + 0.5 * sin(wave + spoke);
|
||||
v = mix(v, smoothstep(0.2, 0.8, v), u_softness);
|
||||
|
||||
vec3 col = palRamp(t * u_colorRoll + d * 0.25) * v;
|
||||
|
||||
// Core glow, the part that reads as the "hit".
|
||||
col += pal(0) * (1.0 - smoothstep(0.0, 0.7, d)) * u_bloomCore * 0.6;
|
||||
|
||||
// Keep the corners from clipping to flat colour.
|
||||
col *= 0.6 + 0.4 * (1.0 - smoothstep(0.8, 1.8, d));
|
||||
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default classicWave;
|
||||
@@ -0,0 +1,66 @@
|
||||
// Ported from party-stage's "Floating Geometry". Shape count, size and motion
|
||||
// were fixed constants in the original; they are the whole point of the scene,
|
||||
// so they are now params the look generator can move.
|
||||
|
||||
export const floatingGeometry = {
|
||||
name: 'Floating Geometry',
|
||||
family: 'geometric',
|
||||
kind: 'fragment',
|
||||
|
||||
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' },
|
||||
spin: { type: 'float', range: [0, 2], default: 0.6, uniform: 'u_spin' },
|
||||
boxRatio: { type: 'float', range: [0, 1], default: 0.5, uniform: 'u_boxRatio' },
|
||||
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 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
size: { feature: 'beat', amount: 0.18, response: 'spike' },
|
||||
aura: { feature: 'bandHigh', amount: 0.4 },
|
||||
spin: { feature: 'bandLow', amount: 0.2 },
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
// 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);
|
||||
|
||||
for (int i = 0; i < 14; i++) {
|
||||
if (i >= u_count) break;
|
||||
float fi = float(i);
|
||||
float s = u_seed + fi * 123.456;
|
||||
|
||||
vec2 pos = vec2(
|
||||
sin(t * 0.5 + s) * u_spread,
|
||||
cos(t * 0.3 + s * 1.1) * u_spread * 0.55
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
vec3 shapeColor = pal(i + int(floor(t * 0.3)));
|
||||
float intensity = smoothstep(0.012, 0.0, d);
|
||||
col = mix(col, shapeColor, intensity * 0.85);
|
||||
col += shapeColor * (1.0 - smoothstep(0.0, size * 2.2, abs(d))) * u_aura;
|
||||
}
|
||||
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default floatingGeometry;
|
||||
@@ -0,0 +1,133 @@
|
||||
// Ported from party-stage's "Drugged Out Flow".
|
||||
//
|
||||
// The original ran its own 15-second internal scene timer to keep itself
|
||||
// interesting. That job now belongs to the arc driver, which knows where the
|
||||
// song's actual transitions are — so the internal timer is gone and the scene
|
||||
// variety is driven by `variant`, a param the arc can step.
|
||||
|
||||
export const psychedelicDrift = {
|
||||
name: 'Psychedelic Drift',
|
||||
family: 'glitch',
|
||||
kind: 'fragment',
|
||||
|
||||
params: {
|
||||
count: { type: 'int', range: [4, 24], default: 12, uniform: 'u_count', bias: 'density' },
|
||||
variant: { type: 'float', range: [0, 8], default: 0, uniform: 'u_variant' },
|
||||
warp: { type: 'float', range: [0, 0.6], default: 0.2, uniform: 'u_warp', bias: 'energy' },
|
||||
beams: { type: 'int', range: [0, 5], default: 3, uniform: 'u_beams' },
|
||||
symbolSize: { type: 'float', range: [0.03, 0.14], default: 0.07, uniform: 'u_symbolSize' },
|
||||
speed: { type: 'float', range: [0.05, 0.8], default: 0.2, uniform: 'u_speed', bias: 'motion' },
|
||||
palette: { type: 'palette', count: 6 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
warp: { feature: 'beat', amount: 0.35, response: 'spike' },
|
||||
symbolSize: { feature: 'bandLow', amount: 0.25 },
|
||||
speed: { feature: 'buildSlope', amount: 0.4, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
float sdCircle(vec2 p, float r) { return length(p) - r; }
|
||||
|
||||
float sdStar(vec2 p, float r, float points) {
|
||||
float a = atan(p.y, p.x);
|
||||
float seg = 6.28318530718 / points;
|
||||
a = mod(a + seg * 0.5, seg) - seg * 0.5;
|
||||
return length(p) * cos(a) - r * 0.5;
|
||||
}
|
||||
|
||||
float sdSmiley(vec2 p, float r, float t, float id) {
|
||||
float d = sdCircle(p, r);
|
||||
float blink = smoothstep(0.9, 0.95, sin(t * 0.5 + id * 1.2));
|
||||
float eyeR = 0.18 * r * (1.0 - blink * 0.9);
|
||||
d = max(d, -sdCircle(vec2(abs(p.x) - 0.3 * r, p.y - 0.35 * r), eyeR));
|
||||
|
||||
float mouthType = sin(t * 1.2 + id);
|
||||
if (mouthType > 0.6) {
|
||||
d = max(d, -sdCircle(p - vec2(0.0, -0.2) * r, 0.15 * r));
|
||||
} else if (mouthType > -0.6) {
|
||||
float mouth = sdCircle(p - vec2(0.0, -0.1) * r, 0.45 * r);
|
||||
mouth = max(mouth, -(p.y - (0.05 + 0.1 * u_beat) * r));
|
||||
d = max(d, -mouth);
|
||||
} else {
|
||||
d = max(d, -sdCircle(p - vec2(0.0, -0.25) * r, 0.08 * r));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
float distToSegment(vec2 p, vec2 a, vec2 b) {
|
||||
vec2 pa = p - a, ba = b - a;
|
||||
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
||||
return length(pa - ba * h);
|
||||
}
|
||||
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_speed + u_seed;
|
||||
float beat = u_beat;
|
||||
float vs = u_seed + floor(u_variant) * 100.0;
|
||||
|
||||
// Swirling background.
|
||||
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;
|
||||
|
||||
// Lens distortion that drags everything toward a wandering centre.
|
||||
vec2 centre = vec2(sin(t * 0.3 + vs) * 0.5, cos(t * 0.4 + vs * 1.1) * 0.5);
|
||||
float radius = 0.35 + beat * 0.15;
|
||||
float dc = length(p - centre);
|
||||
vec2 dp = p;
|
||||
if (dc < radius && dc > 1e-4) {
|
||||
dp += normalize(p - centre) * smoothstep(radius, 0.0, dc) * (u_warp + beat * 0.2);
|
||||
}
|
||||
|
||||
// Beam endpoints, precomputed so symbols can react to them.
|
||||
vec2 b1[5], b2[5];
|
||||
for (int j = 0; j < 5; j++) {
|
||||
float ls = vs + float(j) * 789.0;
|
||||
b1[j] = vec2(sin(t * 0.4 + ls) * 0.8, cos(t * 0.3 + ls * 1.1) * 0.5);
|
||||
b2[j] = vec2(sin(t * 0.5 + ls * 1.5) * 0.8, cos(t * 0.4 + ls * 1.7) * 0.5);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 24; i++) {
|
||||
if (i >= u_count) break;
|
||||
float fi = float(i);
|
||||
float s = vs + fi * 123.456;
|
||||
|
||||
float speed = 0.3 + hash11(s) * 0.4;
|
||||
vec2 basePos = vec2(sin(t * speed + s) * 0.8, cos(t * speed * 0.8 + s * 1.4) * 0.5);
|
||||
vec2 pos = mix(basePos, centre, 0.15 + beat * 0.1);
|
||||
|
||||
vec2 sp = rot(t * (0.1 + fract(s * 0.7))) * (dp - pos);
|
||||
float size = u_symbolSize * (0.7 + fract(s * 0.3) * 0.6);
|
||||
|
||||
float d;
|
||||
float kind = mod(fi + floor(u_variant), 3.0);
|
||||
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);
|
||||
|
||||
float hit = 0.0;
|
||||
for (int j = 0; j < 5; j++) {
|
||||
if (j >= u_beams) break;
|
||||
hit = max(hit, smoothstep(0.15, 0.0, distToSegment(pos, b1[j], b2[j])));
|
||||
}
|
||||
|
||||
vec3 sc = mix(pal(i + int(floor(t * 0.3))), vec3(1.0), hit * 0.8);
|
||||
float intensity = smoothstep(0.012, 0.0, d);
|
||||
col = mix(col, sc, intensity * sat(0.7 + beat * 0.3 + hit));
|
||||
col += sc * (1.0 - smoothstep(0.0, size * 2.5, abs(d))) * 0.15 * (0.5 + beat * 0.5 + hit * 2.0);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (i >= u_beams) break;
|
||||
float w = 0.008 + beat * 0.005;
|
||||
float intensity = smoothstep(w, 0.0, distToSegment(dp, b1[i], b2[i]));
|
||||
col += pal(i + 2) * intensity * (0.6 + beat);
|
||||
}
|
||||
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default psychedelicDrift;
|
||||
@@ -0,0 +1,107 @@
|
||||
// Ported from party-stage's "Synthwave Run".
|
||||
//
|
||||
// The original hardcoded magenta and cyan and drew a car silhouette with
|
||||
// red taillights. The car is kept — it is the scene's whole identity — but the
|
||||
// colours now come from the palette, and the elements (grid, mountains, sun,
|
||||
// car) are individually switchable so the arc driver can strip it back to just
|
||||
// the grid during a breakdown and bring the rest in for the drop.
|
||||
|
||||
export const synthwaveRun = {
|
||||
name: 'Synthwave Run',
|
||||
family: 'structural',
|
||||
kind: 'fragment',
|
||||
|
||||
params: {
|
||||
speed: { type: 'float', range: [0.3, 4], default: 1.5, uniform: 'u_speed', bias: 'motion' },
|
||||
gridDensity:{ type: 'float', range: [0.5, 3], default: 1.0, uniform: 'u_gridDensity', bias: 'density' },
|
||||
horizon: { type: 'float', range: [-0.3, 0.3],default: 0.0, uniform: 'u_horizon' },
|
||||
sun: { type: 'float', range: [0, 1], default: 1.0, uniform: 'u_sun' },
|
||||
mountains: { type: 'float', range: [0, 1], default: 1.0, uniform: 'u_mountains' },
|
||||
car: { type: 'float', range: [0, 1], default: 1.0, uniform: 'u_car' },
|
||||
glow: { type: 'float', range: [0, 1.5], default: 0.5, uniform: 'u_glow', bias: 'energy' },
|
||||
palette: { type: 'palette', count: 4 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
glow: { feature: 'beat', amount: 0.45, response: 'spike' },
|
||||
speed: { feature: 'bandLow', amount: 0.3 },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_speed + u_seed;
|
||||
float beat = u_beat;
|
||||
float horizon = u_horizon;
|
||||
|
||||
vec3 colorMain = pal(0);
|
||||
vec3 colorSec = pal(1);
|
||||
vec3 col = pal(3) * 0.08;
|
||||
|
||||
// --- perspective ground grid ---
|
||||
if (p.y < horizon) {
|
||||
float perspective = 1.0 / (horizon - p.y + 0.05);
|
||||
vec2 g = vec2(p.x * perspective, perspective + t * 2.0) * u_gridDensity;
|
||||
float lines = smoothstep(0.05, 0.0, abs(fract(g.x + 0.5) - 0.5))
|
||||
+ smoothstep(0.05, 0.0, abs(fract(g.y + 0.5) - 0.5));
|
||||
col = mix(col, colorSec, lines * 0.5 * (0.5 + beat));
|
||||
if (abs(p.x * perspective) < 0.8) col += colorMain * 0.15 * perspective;
|
||||
}
|
||||
|
||||
// --- wireframe mountains ---
|
||||
if (u_mountains > 0.01 && p.y > horizon - 0.1) {
|
||||
float mx = abs(p.x) - 0.5;
|
||||
if (mx > 0.0) {
|
||||
float h = vnoise(vec2(mx * 2.0 + t * 0.1, u_seed)) * 0.6 * smoothstep(0.0, 0.5, mx);
|
||||
if (p.y - horizon < h) {
|
||||
float edge = abs(p.y - horizon - h);
|
||||
float wire = smoothstep(0.02, 0.0, edge)
|
||||
+ smoothstep(0.01, 0.0, abs(fract((p.y - horizon) * 10.0) - 0.5))
|
||||
+ smoothstep(0.01, 0.0, abs(fract(mx * 10.0) - 0.5));
|
||||
col = mix(col, colorMain, wire * 0.4 * u_mountains);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- retro sun with scanline cutouts ---
|
||||
if (u_sun > 0.01) {
|
||||
vec2 sunPos = vec2(0.0, 0.1);
|
||||
float sunRad = 0.4;
|
||||
float dSun = length(p - sunPos);
|
||||
if (dSun < sunRad && p.y > horizon) {
|
||||
float scan = sin((p.y - t * 0.1) * 50.0);
|
||||
if (scan < mix(0.8, -1.0, (p.y - sunPos.y + sunRad) / (sunRad * 2.0))) {
|
||||
vec3 sunCol = mix(colorMain, pal(2), sat(p.y * 2.0));
|
||||
col = mix(col, sunCol, (0.8 + beat * 0.2) * u_sun);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- car silhouette ---
|
||||
if (u_car > 0.01) {
|
||||
vec2 carPos = vec2(sin(t * 0.1) * 0.2, -0.78 + sin(t * 30.0) * 0.001);
|
||||
vec2 cp = p - carPos;
|
||||
float cpX = abs(cp.x);
|
||||
|
||||
float body = step(cpX, 0.35) * step(abs(cp.y), 0.12);
|
||||
float glass = step(cpX, 0.22) * step(abs(cp.y - 0.18), 0.07);
|
||||
float wingTop = step(cpX, 0.32) * step(abs(cp.y - 0.20), 0.025);
|
||||
float wingSide = step(abs(cpX - 0.30), 0.03) * step(abs(cp.y - 0.12), 0.1);
|
||||
|
||||
if (body + glass + wingTop + wingSide > 0.0) {
|
||||
col = mix(col, vec3(0.015), 0.95 * u_car);
|
||||
float r = 0.025;
|
||||
float l1 = smoothstep(r, r * 0.6, length(vec2(cpX - 0.21, cp.y - 0.03)));
|
||||
float l2 = smoothstep(r, r * 0.6, length(vec2(cpX - 0.29, cp.y - 0.03)));
|
||||
col = mix(col, pal(2) * (0.6 + beat * 1.4), sat(l1 + l2) * u_car);
|
||||
}
|
||||
}
|
||||
|
||||
// Horizon haze, scaled by the glow param.
|
||||
col += colorMain * exp(-abs(p.y - horizon) * 8.0) * u_glow * 0.35;
|
||||
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default synthwaveRun;
|
||||
Reference in New Issue
Block a user