// Geometric family: an Apollonian packing — a circle filled with circles, each
// gap filled again with smaller ones, forever. Built by inversion rather than by
// drawing, so the nesting is exact at every scale the frame can resolve.
//
// Quasicrystal is the other infinitely-detailed geometric and its detail is
// additive: waves summed until the interference looks complicated. This detail
// is structural — every disc has a definite size and place, and zooming finds
// more of them rather than finer noise. Nothing here is a texture, which is why
// it holds up when the shot pushes in.

export const apollonianGasket = {
    name: 'Apollonian Gasket',
    family: 'geometric',
    kind: 'fragment',
    // Drawn geometry; grain only muddies the small discs.
    texture: 0.3,
    consumes: ['ink'],
    traits: ['camera', 'style'],

    params: {
        depth:   { type: 'int',   range: [3, 12],      default: 7,    uniform: 'u_depth', bias: 'density' },
        pack:    { type: 'float', range: [0.9, 1.6],   default: 1.2,  uniform: 'u_pack' },
        offset:  { type: 'float', range: [0, 0.35],    default: 0.2,  uniform: 'u_offset' },
        zoom:    { type: 'float', range: [0.4, 2.2],   default: 1.0,  uniform: 'u_zoom', slowAxis: true },
        rim:     { type: 'float', range: [0, 1.5],     default: 0.7,  uniform: 'u_rim', bias: 'energy' },
        churn:   { type: 'float', range: [0.004, 0.05], default: 0.015, uniform: 'u_churn', bias: 'motion', rate: true },
        palette: { type: 'palette', count: 5 },
    },

    reactive: {
        rim:    { feature: 'bandHigh', amount: 0.35, response: 'smooth' },
        offset: { feature: 'bandLow',  amount: 0.12, response: 'smooth' },
    },

    shader: `
vec4 scene(vec2 uv, vec2 p) {
    float t = u_time * u_churn + u_seed;
    p = sigCamera(p);
    p /= max(u_zoom, 0.05);

    // The Kleinian fold: reflect into a fundamental domain, then invert through
    // the unit circle, over and over. Each pass divides the scale by the amount
    // the inversion contracted, and the accumulated scale is what turns the
    // whole recursion back into a single distance at the end.
    float scale = 1.0;
    vec2 q = p;
    float touched = 0.0;

    // lint: fixed-cost — u_depth inversions, bounded at fourteen
    for (int i = 0; i < 14; i++) {
        if (i >= u_depth) break;
        // Stop when the next generation would be finer than the frame can
        // resolve. Without this the deep discs alias into a speckle that reads
        // as noise, which is the opposite of what an exact packing is for.
        if (scale > 45.0) break;

        // Fold into the strip: this is what makes the packing infinite in
        // every direction rather than one circle's worth.
        q = mod(q + 1.0, 2.0) - 1.0;
        q -= vec2(u_offset * sin(t + float(i) * 0.7), u_offset * cos(t * 0.8)) * 0.25;

        float r2 = dot(q, q);
        float k = u_pack / max(r2, 1e-4);
        q *= k;
        scale *= k;
        touched += 1.0;
    }

    // Back to a screen-space distance. Without dividing by the accumulated
    // scale the small discs would be drawn with the same line weight as the
    // large ones and the image would be a solid mat of edges.
    float d = (length(q) - 1.0) / scale;

    float w = 0.006 + u_sigLine * 0.03;
    float edge = smoothstep(w * 2.0, w * 0.3, abs(d));
    float glow = exp(-abs(d) * 26.0);

    // The deepest generations are held back rather than cut off. A hard depth
    // limit makes discs pop in and out as the fold slides across it, and a
    // frame-wide population of discs appearing at once measured as a strobe.
    float lod = smoothstep(45.0, 18.0, scale);
    edge *= mix(0.25, 1.0, lod);
    glow *= mix(0.15, 1.0, lod);

    // Colour by how deep the point fell before it settled, so the generations
    // of the packing are legible as bands rather than all being one hue.
    float gen = fract(log2(max(scale, 1e-6)) * 0.15 + 0.5);

    vec3 col = pal(0) * 0.06;
    col += palRamp(gen) * edge * (0.55 + u_rim * 0.7);
    col += palRamp(gen + 0.25) * glow * u_rim * 0.4;
    col += pal(4) * inkStroke(d) * 0.5;

    // Fill the interiors faintly so the discs are objects and not just outlines.
    col += palRamp(gen + 0.5) * smoothstep(0.0, -0.06, d) * 0.12;

    col *= 0.7 + 0.3 * exp(-dot(p, p) * 0.2);
    return vec4(inkValue(col), 1.0);
}
`,
};

export default apollonianGasket;
