music-video-gen/flow-state/src/scenes/shader/psychedelic-drift.js
Dejvino 1ca9fa2f1f Let scenes layer on each other, and give the camera somewhere to look
Two devices that were built, wired up, and doing nothing.

OVERLAYS. buildStack gated layering on `surfaceOf(m) === 'composable'`, and
no scene in the library declared `surface` — so the only thing that could ever
sit on top was the one scene declaring `role: 'accent'`, and the overlay roster
excluded accents by construction. Empty intersection, every time: 0/144 stacks
carried an overlay. Every layered frame in every song was the same particle
field.

Labelled the library from the phase 12 coverage gate rather than by eye: the
37 scenes painting under 30% of the frame are composable. Particle Field loses
its privileged slot and becomes one of them, keeping only `background: false`,
which is the honest part — points in empty space cannot carry a section alone.
The reserved accent slot is gone; one roster, two passes at it. 1 distinct
overlay scene becomes 27, and the rate lands at 34% of stacks after trimming a
base chance that had been tuned while the branch was dead.

THE CAMERA. framing.shift moved the frame by a median of 0.029 of a half-frame
at a fresh random angle every shot, so successive offsets cancelled and the
median jump at a cut was 0.014. Present in every frame, visible in none.

look/Camera.js is the director's camera department: the story says tension,
order and which act a section is in, and this turns that into where the frame
looks and how it travels there. Each director names a camera. Jump distance
follows tension and act, speed follows energy, and the curve is one of four.
Cuts choose between reframing and matching, so two scenes can still read as one
place. Median offset 0.190, median reframe 0.135, and 94% of shots now move
during the shot rather than only at the cut.

Four new gates, each with a FLOOR — the device this replaces passed every
existing check while doing nothing, because the only bound was a ceiling.

Also lands the in-progress Epic 4 story layer it builds on: Story.js, phase 13,
and the direction statistic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 06:48:13 +02:00

141 lines
5.4 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',
// Paints 6% of the frame — see checks/phase12 coverage.
surface: 'composable',
// Personality: see look/Personality.js.
consumes: ['cast', 'ink'],
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 = castMain(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);
}
return vec4(inkValue(col), 1.0);
}
`,
};
export default psychedelicDrift;