// Minimal family: a sparse field of horizontal lines that bend around the
// centre. Most of the frame is negative space, which is exactly what an intro or
// a breakdown wants — the arc driver has nowhere restful to go otherwise.

export const horizonLines = {
    name: 'Horizon Lines',
    family: 'minimal',
    kind: 'fragment',
    // Personality: see look/Personality.js.
    // Crisp line work: the track's surface grain would only fur the edges.
    texture: 0,
    traits: ['space', 'camera', 'style'],

    params: {
        count:     { type: 'float', range: [3, 40],    default: 14,  uniform: 'u_count', bias: 'density' },
        thickness: { type: 'float', range: [0.002, 0.03], default: 0.008, uniform: 'u_thickness' },
        bend:      { type: 'float', range: [0, 1.2],   default: 0.35, uniform: 'u_bend' },
        speed:     { type: 'float', range: [0.02, 0.4], default: 0.12, uniform: 'u_speed', bias: 'motion', rate: true },
        spread:    { type: 'float', range: [0.2, 1.4], default: 0.9,  uniform: 'u_spread' },
        glow:      { type: 'float', range: [0, 1],     default: 0.3,  uniform: 'u_glow', bias: 'energy' },
        palette:   { type: 'palette', count: 4 },
    },

    reactive: {
        bend: { feature: 'bandLow', amount: 0.35, response: 'smooth' },
        glow: { feature: 'beat',    amount: 0.3,  response: 'spike' },
    },

    shader: `
vec4 scene(vec2 uv, vec2 p) {
    float t = u_time * u_speed + u_seed;
    p = sigCamera(p);
    // The bundle sits on the track's horizon rather than in the middle of the
    // frame — this scene is nothing but a horizon, so it should be the shared one.
    p.y -= sigHorizonY() * 0.6;

    // Displace vertically by a slow wave, strongest at the centre of the frame.
    float envelope = exp(-p.x * p.x * 1.2);
    float offset = sin(p.x * 2.2 + t * 2.0) * u_bend * envelope;

    vec3 col = pal(0) * 0.06;

    // One pixel, in the scene units p is measured in. p spans [-1, 1] vertically
    // regardless of output size, so this is the exact height of a pixel.
    float px = 2.0 / u_resolution.y;

    for (int i = 0; i < 40; i++) {
        if (float(i) >= u_count) break;
        float fi = float(i);
        float slot = (fi / max(u_count - 1.0, 1.0) - 0.5) * 2.0 * u_spread;

        float y = slot + offset * (0.4 + fract(fi * 0.37));
        float d = abs(p.y - y);

        // Analytic coverage rather than a smoothstep to zero.
        //
        // The width here is genuinely sub-pixel: 0.008 scene units against a
        // 0.028-unit pixel at 720p is a line under a third of a pixel wide, and
        // the range goes down to a fourteenth. Ramping from full to nothing
        // across a third of a pixel is a near-vertical cliff, and a cliff turns
        // any float wobble in d into a whole byte of colour — which is what
        // made this the one scene in the library that failed the determinism
        // gate, intermittently, at 2/255.
        //
        // Coverage fixes the cause rather than the symptom. The transition
        // always spans exactly one pixel, so nothing sits on a discontinuity;
        // and a line thinner than a pixel now comes out DIM rather than being
        // drawn at full brightness wherever a pixel centre happens to land on
        // it, which is both correct and the end of the shimmer it used to have.
        // Width stays in scene units, so resolution independence is unchanged.
        float w = u_thickness * (0.5 + u_sigLine);
        float line = sat((w - d) / px + 0.5);
        float halo = exp(-d * 26.0) * u_glow;

        vec3 c = pal(i);
        col += c * (line + halo * 0.55);
    }

    // Keep the far edges dark so the lines read as a subject, not wallpaper.
    col *= 0.55 + 0.45 * exp(-dot(p, p) * 0.5);
    col = sigAir(col, p, smoothstep(0.0, 1.6, abs(p.y)));
    col += sigGrain(uv);
    return vec4(col, 1.0);
}
`,
};

export default horizonLines;
