music-video-gen/flow-state/src/scenes/shader/silk-ribbon.js
Dejvino 17a583a87f Side quest 1: give four scenes art direction other than grain
Classic Wave, Silk Ribbon, Kaleido Tunnel and Slow Orb all declared the
`style` trait and honoured it with `col += sigGrain(uv)` and nothing else. A
declared trait is a contract — the disqualification rule in Personality.js is
the only thing keeping off-design scenes out of a track — so honouring it
with dirt meant these four could not take the `texture: 0` opt-out the grain
work introduced. Phase 9 measured their style response at exactly 0. They sat
at texture: 0.35 as a stopgap, which kept speckle on the library's cleanest
scenes purely to keep a gate green.

Each already had the knob; it just was not wired to the track:

- Classic Wave contrasts its wave through a bare smoothstep(0.2, 0.8). The
  transition width now comes from u_sigSoft and u_sigLine, centred on 0.5 so
  changing the hand does not change the exposure. Crest concentration is
  driven separately by u_sigLine, because a track is free to sample the
  scene's own u_softness at zero and the art direction must still show.
- Silk Ribbon's strand width is u_sigLine and its falloff exponent u_sigSoft:
  a sharp track gets a filament with a defined edge, a soft one a haze.
- Kaleido Tunnel drew its grid against a bare 0.42 — a line weight with no
  name. It is now a weight and a feather, both from the track.
- Slow Orb's body edge multiplies the scene's softness by the video's, and
  gains a sigEdge rim so a sharp-handed track gets a defined limb.

All four are now texture: 0. Style response measured against a maximally
soft-handed versus maximally sharp-handed personality: Classic Wave 111,
Silk Ribbon 216, Kaleido Tunnel 206, Slow Orb 102, out of 255 — previously 0.

104/104 checks pass including the slow set. See SIDE-QUESTS.md §1.

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

80 lines
3.4 KiB
JavaScript

// Minimal family: a single ribbon of light draping across an almost empty
// frame. The curve is a closed-form travelling wave, so it is seek-exact; it is
// drawn by sampling the polyline and taking the minimum distance, which reads as
// one fluid strand rather than a repeated motif. Most of the frame is negative
// space — what an intro or a breakdown needs.
export const silkRibbon = {
name: 'Silk Ribbon',
family: 'minimal',
kind: 'fragment',
// Personality: see look/Personality.js.
// A soft drape of light. Grain furs the one thing it is made of.
texture: 0,
traits: ['camera', 'style'],
params: {
thickness: { type: 'float', range: [0.008, 0.08], default: 0.03, uniform: 'u_thickness' },
wave: { type: 'float', range: [0.1, 2.5], default: 1.1, uniform: 'u_wave' },
sway: { type: 'float', range: [0.05, 0.6], default: 0.25, uniform: 'u_sway', bias: 'motion' },
glow: { type: 'float', range: [0, 1.5], default: 0.5, uniform: 'u_glow', bias: 'energy' },
speed: { type: 'float', range: [0.02, 0.4], default: 0.12, uniform: 'u_speed', bias: 'motion', rate: true },
spread: { type: 'float', range: [0.5, 1.2], default: 0.9, uniform: 'u_spread' },
palette: { type: 'palette', count: 4 },
},
reactive: {
glow: { feature: 'beat', amount: 0.3, response: 'smooth' },
wave: { feature: 'bandLow', amount: 0.25, response: 'smooth' },
},
shader: `
// Nearest distance from p to a fixed travelling strand.
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_speed + u_seed;
p = sigCamera(p);
// Bare wash so the frame is never black.
vec3 col = pal(0) * 0.04;
// Two nested strands, offset in phase so the ribbon reads as a drape with a
// folded edge rather than a single hairline.
for (int k = 0; k < 2; k++) {
float fk = float(k);
float ph = fk * 2.2 + t * 0.3;
float best = 1e9;
// lint: fixed-cost — this samples the curve at a fixed resolution, so
// there is no param to break on. Cost is governed by the 4K budget gate.
for (int n = 0; n < 48; n++) {
float uu = (float(n) + 0.5) / 48.0;
float x = (uu - 0.5) * 2.0 * u_spread;
float y = sin(uu * 6.28318530718 * u_wave + ph) * 0.28
+ sin(uu * 13.1 - t * 0.8 + fk) * 0.06
+ u_sway * 0.4 * sin(t * 0.6 + fk);
float d = length(p - vec2(x, y));
best = min(best, d);
}
// TRAIT style: the ribbon is drawn in the track's hand. u_sigLine sets
// how wide the strand is, u_sigSoft how fast it falls off — a sharp
// track gets a filament with a defined edge, a soft one gets a haze.
// This is the scene's whole art direction, so it is not scaled by any
// of its own params: there is no setting at which the track stops
// showing.
float w = u_thickness * mix(0.55, 1.7, sat(u_sigLine));
float falloff = mix(2.6, 0.9, sat(u_sigSoft));
vec3 hc = pal(int(fk) % 3);
col += hc * exp(-pow(best / w, falloff * 2.0));
col += pal(3) * exp(-best * best * 60.0) * u_glow * 0.6;
}
// Knot the ribbon just below frame centre so it has somewhere to pull from.
float knot = length(p - vec2(0.0, 0.1));
col = mix(col, pal(2), exp(-knot * knot * 30.0) * u_glow * 0.5);
col += sigGrain(uv);
return vec4(col, 1.0);
}
`,
};