music-video-gen/flow-state/src/scenes/shader/classic-wave.js
Dejvino 2d3ed8eb02 Finish the migration: all 65 scenes on the identity artifacts
Two helpers made the bulk of it mechanical. `inkStroke` is a drop-in for
sigEdge — the same line at the identity's weight rather than the track's — and
`castForm` is a drop-in for sigForm, same signature so call sites do not change
shape. With those in place the substitution table is one-to-one:

    sigForm(  -> castForm(     sigShape( -> castMain(     sigEdge( -> inkStroke(

Forty-seven scenes went through that pass in one run: twenty-six take the cast
and the ink, twenty-one take the ink alone. Then the gate ran over all sixty-five
and found three the pass had broken, which is the entire reason it exists.

Spectrum Sculpture rendered pure black. It had been using sigShape as a RADIAL
METRIC rather than drawing it, and the cast carries notches and a hollow — an
annulus used as a radius turns a sculpture inside out. Reverted to sigShape and
dropped to ink only. The lesson generalises: a scene that reads a form as
geometry is not a scene that draws it, and the classifier cannot tell those
apart from the source.

Eclipse Field stopped honouring its `style` trait. It opts out of surface grain,
so sigEdge was its only style evidence, and the ink replaced it. The trait claim
is now dropped — and so is the lint change that had let inkMask count as style
evidence, which was wrong and was hiding exactly this. A trait is a property of
the track a scene may honour; an artifact is content it draws. Taking the ink
says nothing about whether a scene responds to u_sigLine.

Circuit Bloom went empty at the bottom of its `grown` range, where the pads were
carried by a hairline and the ink's stroke is thinner than the edge it replaced.
Now filled as well as stroked.

Also: the backtick check now covers every shader literal rather than only the
preamble, because a mechanical pass over sixty files reintroduced one
immediately. Three rounds lost to that typo is enough.

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

76 lines
3.4 KiB
JavaScript

// 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',
// Personality: see look/Personality.js.
// Smooth concentric colour: grain only mutes the ramp it is built on.
texture: 0,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'],
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', rate: true },
// colorRoll is capped low on purpose. palRamp() steps through all six
// palette entries per unit of t, and the palette generator deliberately
// spreads their luminance — so this multiplies into a whole-frame
// brightness oscillation at six times its own rate. At the original
// range of [0, 0.5] this scene measured 7 flashes per second at every
// output resolution, well past the WCAG 2.3.1 ceiling of 3. Capped here,
// the worst case is ~0.9 Hz. See engine/flash.js.
colorRoll: { type: 'float', range: [0, 0.06], default: 0.02, uniform: 'u_colorRoll', rate: true },
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 },
},
shader: `
vec4 scene(vec2 uv, vec2 p) {
p = sigCamera(p);
// The rings take the track's signature form: round tracks get circles,
// hexagonal tracks get hexagonal rings, and it costs one call.
float d = castMain(p) + 1.0;
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);
// TRAIT style: the track's hand on the crests. The scene's own u_softness
// decides HOW MUCH the wave is contrasted; the track decides what that
// contrast looks like — u_sigSoft widens the transition, u_sigLine tightens
// it. Centred on 0.5 so changing the hand does not change the exposure.
float edge = mix(0.06, 0.42, sat(u_sigSoft)) * mix(1.3, 0.6, sat(u_sigLine));
v = mix(v, smoothstep(0.5 - edge, 0.5 + edge, v), u_softness);
// ...and independently of u_softness, which a track is free to sample at
// zero: a heavier line concentrates the crest rather than letting it bloom
// across the whole ring.
vec3 col = palRamp(t * u_colorRoll + d * 0.25) * pow(v, mix(1.0, 2.2, sat(u_sigLine)));
// 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));
col += sigGrain(uv);
return vec4(inkValue(col), 1.0);
}
`,
};
export default classicWave;