// Minimal family: a large occluder covering a bright source, with the light
// escaping past its edge.
//
// Distinct from Slow Orb (a small soft body drifting through empty space): the
// object here nearly fills the frame, its edge is hard, and it is not the
// subject — the subject is the corona around it. That inversion is the whole
// idea, and it gives the library its one genuinely high-contrast minimal scene:
// almost black, with a thin ring of the brightest thing in the video.
//
// The occluder is the track's signature form, so this is where a hexagonal
// track's hexagon is most legible.
//
// Scaffolded by tools/new-scene.js. See HOWTO-visualizers.md.

export const eclipseField = {
    name: 'Eclipse Field',
    family: 'minimal',
    kind: 'fragment',
    // Crisp line work: the track's surface grain would only fur the edges.
    texture: 0,
    traits: ['shape', 'camera', 'space', 'style'],

    params: {
        size:      { type: 'float', range: [0.2, 0.85],  default: 0.45, uniform: 'u_size' },
        corona:    { type: 'float', range: [0.05, 1.2],  default: 0.45, uniform: 'u_corona', bias: 'energy' },
        rays:      { type: 'float', range: [0, 1],       default: 0.35, uniform: 'u_rays' },
        rayCount:  { type: 'float', range: [3, 24],      default: 9,    uniform: 'u_rayCount', bias: 'density' },
        offset:    { type: 'float', range: [0, 0.5],     default: 0.12, uniform: 'u_offset' },
        drift:     { type: 'float', range: [0.01, 0.3],  default: 0.05, uniform: 'u_drift', bias: 'motion', rate: true },
        sky:       { type: 'float', range: [0, 0.6],     default: 0.15, uniform: 'u_sky' },
        palette:   { type: 'palette', count: 5 },
    },

    reactive: {
        corona: { feature: 'loudness', amount: 0.3,  response: 'smooth' },
        rays:   { feature: 'bandAir',  amount: 0.25, response: 'smooth' },
    },

    shader: `
vec4 scene(vec2 uv, vec2 p) {
    float t = u_time * u_drift + u_seed;
    p = sigCamera(p);

    // The light sits a little off centre from the occluder, so the corona is
    // brighter on one side — a perfectly concentric eclipse looks like a decal.
    vec2 lightAt = vec2(sin(t * 0.8), cos(t * 0.6)) * u_offset;
    vec2 discAt = vec2(0.0, sigHorizonY() * 0.25);

    // Distance to the occluder's edge, in the track's form.
    float d = sigShape((p - discAt) / max(u_size, 1e-3)) * u_size;

    // The source behind it.
    float toLight = length(p - lightAt);
    float source = exp(-toLight * toLight * 2.2);

    vec3 col = pal(0) * u_sky * (1.0 - smoothstep(0.0, 1.6, length(p)));
    col += pal(1) * source * 0.5;

    // Corona: brightest exactly at the edge, falling off outward only. Inside
    // the disc it must be zero, or the occluder stops occluding.
    float outside = smoothstep(0.0, 0.02, d);
    float ring = exp(-max(d, 0.0) / max(u_corona * 0.25, 0.01));
    // The corona breathes on a fixed clock rather than on u_drift. At a low
    // drift — which is most of this scene's range, it is a minimal scene —
    // nothing else in the frame moved enough to register as animation at all.
    float breath = 0.82 + 0.18 * sin(u_time * 0.5 + u_seed * 1.7);
    col += palRamp(0.35 + d * 0.5) * ring * outside * (0.8 + u_corona) * breath;

    // Rays: angular streaks in the corona, rotating slowly. Confined to the
    // corona by the same falloff, so they never light the whole frame.
    if (u_rays > 0.01) {
        float ang = atan(p.y - discAt.y, p.x - discAt.x);
        float spokes = 0.5 + 0.5 * sin(ang * u_rayCount + t * 2.0 + u_time * 0.3);
        col += pal(4) * pow(spokes, 3.0) * ring * outside * u_rays;
    }

    // The occluder: not pure black, so it reads as an object rather than a hole.
    col = mix(col, pal(0) * 0.12, smoothstep(0.004, -0.004, d));
    col += pal(3) * sigEdge(d) * (0.4 + u_corona * 0.5);

    col = sigAir(col, p, smoothstep(0.0, 1.7, length(p)));
    col += sigGrain(uv);
    return vec4(col, 1.0);
}
`,
};

export default eclipseField;
