// Glitch family: a stack of oscilloscope traces losing signal. // // Distinct from Scan Tear (rows displaced sideways), Block Mosh (block-level // datamosh) and Pitch Shatter (vertical transposition): nothing here is // displaced at all. The corruption is in the SIGNAL — each trace degrades from a // clean wave into noise as its lock is lost, and regains it. Loss of lock steps // on the bar grid, so traces drop out in time rather than flickering. export const signalDecay = { name: 'Signal Decay', family: 'glitch', kind: 'fragment', consumes: ['ink'], traits: ['camera', 'style'], params: { traces: { type: 'int', range: [2, 10], default: 5, uniform: 'u_traces', bias: 'density' }, amplitude:{ type: 'float', range: [0.02, 0.3], default: 0.1, uniform: 'u_amplitude', bias: 'energy' }, frequency:{ type: 'float', range: [1, 22], default: 7, uniform: 'u_frequency', bias: 'density' }, loss: { type: 'float', range: [0, 0.9], default: 0.35, uniform: 'u_loss' }, hiss: { type: 'float', range: [0, 1], default: 0.4, uniform: 'u_hiss' }, persist: { type: 'float', range: [0, 0.85], default: 0.4, uniform: 'u_persist' }, // The damage front — see the shader. `frontAt` is the slow axis: it // walks the boundary down the stack over the whole track, so which // lanes are healthy is a different answer at four minutes than at one. front: { type: 'float', range: [0, 1], default: 0.7, uniform: 'u_front', bias: 'density' }, frontAt: { type: 'float', range: [-1.3, 1.3], default: 0.0, uniform: 'u_frontAt', slowAxis: true }, speed: { type: 'float', range: [0.1, 2.0], default: 0.6, uniform: 'u_speed', bias: 'motion', rate: true }, palette: { type: 'palette', count: 5 }, }, reactive: { amplitude: { feature: 'bandLow', amount: 0.35, response: 'smooth' }, hiss: { feature: 'flatness', amount: 0.3 }, }, shader: ` vec4 scene(vec2 uv, vec2 p) { float t = u_time * u_speed + u_seed; p = sigCamera(p); // Quantised era: lock is lost and regained on the eighth-bar grid, so // dropouts land with the music instead of crawling. float era = floor(u_barPhase * 8.0) + floor(t * 2.0) * 8.0; vec3 col = pal(0) * 0.05; float span = 2.0 / max(float(u_traces), 1.0); for (int i = 0; i < 10; i++) { if (i >= u_traces) break; float fi = float(i); float centre = -1.0 + span * (fi + 0.5); float s = u_seed + fi * 27.7; // How much lock this trace has this era. Below zero it is pure noise. float lock = sat(hash12(vec2(fi, era)) * 1.4 - u_loss); // A DAMAGE FRONT across the stack: lock falls away on one side of a // boundary, so the stack has a shape instead of every lane being an // independent coin flip. Independent lanes are why this scene measured // as churn — the arrangement was statistically the same at every moment // even though individual lanes were dropping in and out constantly. // Structure is what the eye can follow; per-lane randomness is not. float side = sat((centre - u_frontAt) * 1.6 + 0.5); float health = mix(1.0, side, u_front); lock = sat(lock * health); // Clean signal: two sines and a slow envelope, so it looks like a // waveform rather than a test tone. float clean = sin(p.x * u_frequency + t * 3.0 + s) * 0.6 + sin(p.x * u_frequency * 2.7 - t * 1.7 + s * 1.3) * 0.4; // Noise floor: hashed per pixel column and era, held steady within a // step so it reads as static rather than as a shimmer. float noise = (hash12(vec2(floor(p.x * 220.0), era + fi)) - 0.5) * 2.0; float signal = mix(noise, clean, lock); float y = centre + signal * u_amplitude; float d = abs(p.y - y); float w = 0.004 + u_sigLine * 0.012; float line = smoothstep(w * 2.5, 0.0, d); vec3 tint = palRamp(fract(s) * 0.4 + 0.15); col += tint * line * (0.4 + lock * 0.6); col += tint * exp(-d * 40.0) * 0.25 * lock; // Hiss band around an unlocked trace: the visual equivalent of the // sound. Confined to the lane, so it never washes the whole frame. // Hiss RISES as lock falls, so without this the front cancelled itself // out: a damaged lane simply traded signal for noise and carried the // same energy. Beyond the front a lane goes quiet rather than noisy. if (lock < 0.4) { float band = exp(-abs(p.y - centre) * 12.0); col += pal(3) * band * abs(noise) * u_hiss * 0.3 * (1.0 - lock) * health; } col *= mix(1.0, 0.25 + 0.75 * health, u_front * 0.6); } // Ghost of the previous frame, so a dropout leaves a trail rather than // vanishing cleanly. col = max(col, prev(uv) * u_persist); return vec4(inkValue(col), 1.0); } `, }; export default signalDecay;