// Minimal family: a single ribbon of light draping across an almost empty
// frame. The curve is a closed-form travelling wave, so it is seek-exact; it is
// drawn by sampling the polyline and taking the minimum distance, which reads as
// one fluid strand rather than a repeated motif. Most of the frame is negative
// space — what an intro or a breakdown needs.

export const silkRibbon = {
    name: 'Silk Ribbon',
    family: 'minimal',
    kind: 'fragment',
    // Personality: see look/Personality.js.
    // A soft drape of light. Grain furs the one thing it is made of.
    texture: 0,
    traits: ['camera', 'style'],

    params: {
        thickness: { type: 'float', range: [0.008, 0.08], default: 0.03, uniform: 'u_thickness' },
        wave:      { type: 'float', range: [0.1, 2.5],    default: 1.1, uniform: 'u_wave' },
        sway:      { type: 'float', range: [0.05, 0.6],   default: 0.25, uniform: 'u_sway', bias: 'motion' },
        glow:      { type: 'float', range: [0, 1.5],      default: 0.5, uniform: 'u_glow', bias: 'energy' },
        speed:     { type: 'float', range: [0.02, 0.4],   default: 0.12, uniform: 'u_speed', bias: 'motion', rate: true },
        spread:    { type: 'float', range: [0.5, 1.2],    default: 0.9, uniform: 'u_spread' },
        palette:   { type: 'palette', count: 4 },
    },

    reactive: {
        glow: { feature: 'beat',    amount: 0.3, response: 'smooth' },
        wave: { feature: 'bandLow', amount: 0.25, response: 'smooth' },
    },

    shader: `
// Nearest distance from p to a fixed travelling strand.
vec4 scene(vec2 uv, vec2 p) {
    float t = u_time * u_speed + u_seed;
    p = sigCamera(p);

    // Bare wash so the frame is never black.
    vec3 col = pal(0) * 0.04;

    // Two nested strands, offset in phase so the ribbon reads as a drape with a
    // folded edge rather than a single hairline.
    for (int k = 0; k < 2; k++) {
        float fk = float(k);
        float ph = fk * 2.2 + t * 0.3;
        float best = 1e9;

        // lint: fixed-cost — this samples the curve at a fixed resolution, so
        // there is no param to break on. Cost is governed by the 4K budget gate.
        for (int n = 0; n < 48; n++) {
            float uu = (float(n) + 0.5) / 48.0;
            float x = (uu - 0.5) * 2.0 * u_spread;
            float y = sin(uu * 6.28318530718 * u_wave + ph) * 0.28
                    + sin(uu * 13.1 - t * 0.8 + fk) * 0.06
                    + u_sway * 0.4 * sin(t * 0.6 + fk);
            float d = length(p - vec2(x, y));
            best = min(best, d);
        }

        // TRAIT style: the ribbon is drawn in the track's hand. u_sigLine sets
        // how wide the strand is, u_sigSoft how fast it falls off — a sharp
        // track gets a filament with a defined edge, a soft one gets a haze.
        // This is the scene's whole art direction, so it is not scaled by any
        // of its own params: there is no setting at which the track stops
        // showing.
        float w = u_thickness * mix(0.55, 1.7, sat(u_sigLine));
        float falloff = mix(2.6, 0.9, sat(u_sigSoft));
        vec3 hc = pal(int(fk) % 3);
        col += hc * exp(-pow(best / w, falloff * 2.0));
        col += pal(3) * exp(-best * best * 60.0) * u_glow * 0.6;
    }

    // Knot the ribbon just below frame centre so it has somewhere to pull from.
    float knot = length(p - vec2(0.0, 0.1));
    col = mix(col, pal(2), exp(-knot * knot * 30.0) * u_glow * 0.5);

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