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>
139 lines
5.3 KiB
JavaScript
139 lines
5.3 KiB
JavaScript
// 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',
|
|
// Personality: see look/Personality.js.
|
|
traits: ['shape', 'camera', 'style'],
|
|
|
|
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', rate: true },
|
|
palette: { type: 'palette', count: 6 },
|
|
},
|
|
|
|
reactive: {
|
|
warp: { feature: 'beat', amount: 0.35, response: 'spike' },
|
|
symbolSize: { feature: 'bandLow', amount: 0.25 },
|
|
},
|
|
|
|
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.
|
|
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;
|
|
|
|
// 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);
|
|
// 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 = sigShape(sp / max(size, 1e-3)) * 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);
|
|
}
|
|
|
|
col += sigGrain(uv);
|
|
return vec4(col, 1.0);
|
|
}
|
|
`,
|
|
};
|
|
|
|
export default psychedelicDrift;
|