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