music-video-gen/flow-state/src/scenes/shader/psychedelic-drift.js
Dejvino 89e05459c0 Every shot stands on something, and the frame has two ends
A section used to be one scene, and two thirds of the library is composable —
sparse by design, elements ON something. Cast as backgrounds anyway, they left
9 of 40 sampled frames under 20% painted, the darkest at 0.3%: a minute and a
half of a few bright things on black, invisible to every gate because every
gate on the stack was a limit rather than a floor.

Every section now stands on a GROUND: a canvas that fills the frame, cast per
section kind so a shot cut changes the shot and not the world. When the shot
fills the frame itself it IS the ground — two canvases stacked is two pictures
fighting. Above that, a coverage BUDGET: director appetite times the section's
energy times where the story is, capped at two frames' worth of material.

The measured facts move into the repo. scenes/metadata.json is generated from
the gallery — coverage as a shot, coverage as a bed, variety, the structural
profile — tracked in git, stamped with a fingerprint of the scenes and the
metric definitions, and refreshed from gallery.html. `surface` is derived from
it rather than declared; nine scenes claimed `canvas` while painting under a
third of the frame, and declaring it is now a lint error. The generator weights
every layering choice by measured structural distance, because family labels
and the render disagree: two `geometric` scenes can be 0.31 apart and a `flow`
and an `organic` scene 0.04.

The gallery's 0.1 red line is gone. It was right when a section was one scene
and wrong now — nineteen scenes were failing a bar for being consistent, which
is a virtue in an ingredient.

Chasing the numbers turned up four real faults:

  * A scene that reads prev() cannot be a ground. It returns the whole
    composited frame including the layers above it, so a datamosh under a shot
    is eating it: the render stopped reproducing from a seek and two WebGL
    contexts diverged by 91/255 against a tolerance of 4.
  * Screen was the wrong operator for a shot over a bed. It lightens, so a
    median quarter of every frame clipped to paper and whole sections rendered
    100% white. Replaced by a lumakey — the shot's brightness is its alpha.
  * Feedback was an accumulator: a still image settled at 2.3x its own
    brightness. Fine over black, fatal over a filled ground. Normalised at 0.6,
    plus a highlight shoulder so the top rolls off instead of clipping.
  * useTrack never prewarmed, so a fresh Show's first frame differed from every
    later render of it — the export-breaking hazard Compositor.prime documents.

Blazing is a decision now, not a side effect: directors declare an appetite for
it, a section must be loud and late in the story to earn one, and quiet kinds
never do. The ceiling gate matches that — a hard cap per section, and no more
than a fifth of them hot at all.

Rendered across twelve videos, middle of every section:

    painted        51% mean, darkest 0.3%  ->  87% mean, darkest 43%
    clipped white  24% median, worst 100%  ->   1% mean, worst 30%
    separation     0.10                    ->  0.44

Seven scenes can ground a section — five geometric, two organic — so every
quiet section of every video stands on one of two beds. That is the library's
largest hole and it is scene work: there is no minimal or flow canvas that
fills half the frame without reading prev().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 09:02:01 +02:00

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.
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;