// 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',
    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' },
        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);

        // 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.
        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);
        }
    }

    // Ghost of the previous frame, so a dropout leaves a trail rather than
    // vanishing cleanly.
    col = max(col, prev(uv) * u_persist);

    col += sigGrain(uv);
    return vec4(col, 1.0);
}
`,
};

export default signalDecay;
