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