music-video-gen/flow-state/src/scenes/shader/time-smear.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

85 lines
3.9 KiB
JavaScript

// Glitch family: horizontal bands, each holding the image at a different age.
//
// Distinct from every other glitch scene in the library by what it corrupts.
// Scan Tear displaces rows sideways, Pitch Shatter shifts bands vertically,
// Block Mosh drags blocks along a stroke, Signal Decay attacks the signal
// itself. Here NOTHING is displaced: every band shows the same image, just at a
// different moment. The frame is spatially intact and temporally shredded,
// which is a slit-scan — and it is built from the one feedback buffer by giving
// each band its own persistence rather than by keeping a history.
//
// Scaffolded by tools/new-scene.js. See HOWTO-visualizers.md.
export const timeSmear = {
name: 'Time Smear',
family: 'glitch',
kind: 'fragment',
consumes: ['ink'],
traits: ['camera', 'style'],
params: {
bands: { type: 'int', range: [3, 22], default: 9, uniform: 'u_bands', bias: 'density' },
lag: { type: 'float', range: [0.3, 0.97], default: 0.82, uniform: 'u_lag' },
stagger: { type: 'float', range: [0, 1], default: 0.7, uniform: 'u_stagger' },
scale: { type: 'float', range: [1, 12], default: 4.2, uniform: 'u_scale', bias: 'density' },
speed: { type: 'float', range: [0.05, 1.2], default: 0.4, uniform: 'u_speed', bias: 'motion', rate: true },
glow: { type: 'float', range: [0, 1.5], default: 0.5, uniform: 'u_glow', bias: 'energy' },
reshuffle:{ type: 'float', range: [0, 1], default: 0.45, uniform: 'u_reshuffle' },
palette: { type: 'palette', count: 5 },
},
reactive: {
glow: { feature: 'beat', amount: 0.3, response: 'spike' },
scale: { feature: 'bandHigh', amount: 0.2, response: 'smooth' },
},
shader: `
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_speed + u_seed;
vec2 cp = sigCamera(p);
// The subject. It must stand alone: a scene that only reads prev() is black
// until feedback converges and is not the same after a seek as after
// playback. This is a moving figure with enough structure that lagging it
// is legible — a flat field would smear into nothing.
float swirl = fbm(cp * u_scale * 0.4 + vec2(sin(t * 0.7), cos(t * 0.5)), 4);
float arm = sin(atan(cp.y, cp.x) * 3.0 + length(cp) * u_scale - t * 2.5 + swirl * 3.0);
float figure = sat(0.5 + arm * 0.5) * exp(-dot(cp, cp) * 0.55);
vec3 fresh = palRamp(0.1 + swirl * 0.4 + figure * 0.3) * (0.25 + figure * 1.1);
fresh += pal(4) * pow(figure, 5.0) * u_glow;
// Which band this pixel is in, and how old that band's image is. The
// assignment re-rolls on the eighth-bar grid, so the shredding steps with
// the music rather than crawling.
float era = floor(u_barPhase * 8.0) + floor(t * 2.0) * 8.0;
float band = floor(uv.y * float(u_bands));
float rnd = hash12(vec2(band, era * u_reshuffle));
// Age: 0 means live, 1 means as stale as this scene allows. Staggering by
// the hash is what makes neighbouring bands disagree about the present.
float age = mix(band / float(u_bands), rnd, u_stagger);
float persistence = u_lag * (0.35 + age * 0.65);
// The band's own history, held by weighting the previous frame against the
// fresh one. A band at high persistence updates so slowly that it is
// effectively showing several frames ago.
vec3 held = prev(uv);
vec3 col = mix(fresh, held, persistence);
// Seam between bands, drawn in the track's line weight so the shredding
// reads as deliberate rather than as a broken renderer.
float seam = abs(fract(uv.y * float(u_bands)) - 0.5) * 2.0;
col += pal(3) * smoothstep(0.88, 1.0, seam) * (0.05 + u_sigLine * 0.12);
// Stale bands lose colour, the way a held frame loses its highlights.
col = mix(col, vec3(dot(col, vec3(0.299, 0.587, 0.114))), age * 0.35);
col += sigGrain(uv);
return vec4(inkValue(col), 1.0);
}
`,
};
export default timeSmear;