#version 330 core
// Phase 3g -- Circle (mac CircleCrop) iris-style transition between
// clip A and clip B. A circular mask centred at the canvas midpoint
// expands or contracts across the transition window; one clip
// occupies the inside of the mask, the other the outside.
//
// Two TransitionType entries share this shader, distinguished by an
// integer `u_variant` selector (no direction vector -- the circle is
// always concentric to the canvas; the variant selects which side
// of the mask each clip lives on and which way the mask sweeps):
//
//   u_variant = 0  Circle_In  -- B emerges from the centre. The
//                  circle's radius grows 0 -> maxR with progress;
//                  inside the circle is B, outside is A. At
//                  progress=0 the radius is 0 so A covers the
//                  whole canvas; at progress=1 the radius reaches
//                  the diagonal so B covers the whole canvas.
//                  Maps to macOS HTML §5.1 cell #12 "Circle Crop"
//                  (the `CircleCropCenter` wire entry).
//
//   u_variant = 1  Circle_Out -- A shrinks to the centre. The
//                  circle's radius shrinks maxR -> 0 with
//                  progress; inside the circle is A, outside is
//                  B. At progress=0 the radius is maxR so A covers
//                  everything; at progress=1 the radius is 0 so
//                  B covers everything. Maps to macOS HTML §5.1
//                  cell #12 (the `CircleCropCenterReverse` wire
//                  entry).
//
// `maxR = sqrt(0.5)` (~0.7071) is the distance from the canvas
// centre (0.5, 0.5) to a corner (0, 0) / (1, 1) in unit-canvas UV
// space. At progress = 1 the active radius equals maxR which
// guarantees the mask covers every fragment -- a smaller maxR would
// leave the corners of the "outside" clip visible at the final
// frame; a larger maxR would have the mask cross the boundary
// before progress = 1 (asymmetric endpoint behaviour vs the wipe /
// slide / dissolve families that all hit the endpoint at
// progress = 1 exactly).
//
// `band = 0.005` (half-width of the smoothstep around the boundary)
// is a small anti-alias rim that matches the dissolve shader's
// ±0.05 band scaled to the UV-distance domain (the circle boundary
// runs through a ~1-pixel-wide annulus at 1080p, so a tighter band
// avoids a visible fuzzy seam while still removing per-fragment
// jaggies at the moving edge).
//
// `u_progress` is normalised to [0, 1] across the full transition
// window (Phase 2 `sampleTransitionCurve` contract). No
// `u_direction` and no `u_softness` uniform -- the circle is
// omnidirectional, and the smoothstep band is fixed at +/- 0.005 in
// the shader. Per-clip Orientation is honoured by routing each
// sampler's UV through the standard `applyOrient` helper so a
// circle between a flipped A and an upright B doesn't double-flip
// either side (matches `transition_wipe.fs` /
// `transition_slide.fs` / `transition_dissolve.fs` /
// `transition_blur.fs` / `transition_pixelate.fs`).

out vec4 FragColor;
in  vec2 TexCoord;

uniform sampler2D tex_aY;
uniform sampler2D tex_aU;
uniform sampler2D tex_aV;
uniform sampler2D tex_bY;
uniform sampler2D tex_bU;
uniform sampler2D tex_bV;

uniform float u_progress;
uniform int   u_variant;

uniform int   u_aFlipX;
uniform int   u_aFlipY;
uniform float u_aRotRad;
uniform int   u_bFlipX;
uniform int   u_bFlipY;
uniform float u_bRotRad;

// Per-clip Orientation. Verbatim copy of the helper in
// `transition_dissolve.fs` / `transition_wipe.fs` /
// `transition_slide.fs` / `transition_blur.fs` /
// `transition_pixelate.fs` so a circle-between-rotated-clips
// sees the same per-side remap a wipe / dissolve / etc. would.
vec2 applyOrient(vec2 uv, int flipX, int flipY, float rotRad) {
    if (rotRad != 0.0) {
        vec2 c = uv - vec2(0.5);
        float s  = sin(rotRad);
        float co = cos(rotRad);
        uv = vec2(co * c.x - s * c.y, s * c.x + co * c.y) + vec2(0.5);
    }
    if (flipX != 0) uv.x = 1.0 - uv.x;
    if (flipY != 0) uv.y = 1.0 - uv.y;
    return uv;
}

// BT.709 limited-range YUV -> RGB. Identical coefficients to every
// other dual-clip shader so a circle boundary does not shift
// colours relative to the single-clip path on either side of the
// transition.
vec3 yuvToRgb(float y, float u, float v) {
    y = (y - 16.0/255.0)  * (255.0/219.0);
    u = (u - 128.0/255.0) * (255.0/224.0);
    v = (v - 128.0/255.0) * (255.0/224.0);
    float r = y + 1.5748 * v;
    float g = y - 0.1873 * u - 0.4681 * v;
    float b = y + 1.8556 * u;
    return clamp(vec3(r, g, b), 0.0, 1.0);
}

vec3 sampleClip(sampler2D yT, sampler2D uT, sampler2D vT, vec2 uv) {
    // OOR -> letterbox black. The circle shader only samples
    // through applyOrient (no zoom / translate), so OOR can only
    // arise from a non-90-degree orient rotation pushing the UV
    // off the unit square. Matches the dissolve / wipe / slide
    // letterbox-black fallback.
    if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
        return vec3(0.0);
    }
    return yuvToRgb(texture(yT, uv).r,
                    texture(uT, uv).r,
                    texture(vT, uv).r);
}

void main() {
    vec2 uv = TexCoord;

    vec2 uvA = applyOrient(uv, u_aFlipX, u_aFlipY, u_aRotRad);
    vec2 uvB = applyOrient(uv, u_bFlipX, u_bFlipY, u_bRotRad);

    vec3 aRgb = sampleClip(tex_aY, tex_aU, tex_aV, uvA);
    vec3 bRgb = sampleClip(tex_bY, tex_bU, tex_bV, uvB);

    const float maxR = 0.7071067811865476;  // sqrt(0.5), exact at fp32
    const float band = 0.005;
    const vec2  centre = vec2(0.5);

    // Pick the per-variant radius curve and the inside / outside
    // RGBs. Branchless variants would be possible by interpolating
    // the radius along (1 - 2*variant) * progress + variant *
    // maxR, but the explicit if/else reads better and the GPU
    // collapses both paths to a single mix() at the end anyway.
    //
    // Radius range is extended by `band` past each end of the
    // nominal [0, maxR] sweep so the smoothstep boundary fully
    // CLEARS the visible canvas at the two endpoints. Without
    // this extension the center pixel (d = 0) at progress = 0
    // (Circle_In) or progress = 1 (Circle_Out) would land inside
    // the smoothstep band and resolve to mask = 0.5 -- a ~10px
    // 50/50 A/B blob at the canvas centre; symmetrically the 4
    // corner pixels (d = maxR) at progress = 1 (Circle_In) or
    // progress = 0 (Circle_Out) would show 50/50 wedges. The
    // mid-transition curve shifts by < 1px-equivalent at 1080p
    // so the animation timing stays visually identical to the
    // unbiased sweep.
    float pClamped = clamp(u_progress, 0.0, 1.0);
    float radius;
    vec3  inside;
    vec3  outside;
    if (u_variant == 0) {
        // Circle_In: B grows from centre.
        radius  = mix(-band, maxR + band, pClamped);
        inside  = bRgb;
        outside = aRgb;
    } else {
        // Circle_Out: A shrinks to centre, B fills the surround.
        radius  = mix(maxR + band, -band, pClamped);
        inside  = aRgb;
        outside = bRgb;
    }

    float d    = distance(uv, centre);
    // smoothstep returns 0 inside the band (d < radius - band) and
    // 1 outside (d > radius + band); mix(inside, outside, mask)
    // therefore yields `inside` for fragments within the radius
    // and `outside` for fragments beyond it.
    float mask = smoothstep(radius - band, radius + band, d);
    vec3  rgb  = mix(inside, outside, mask);

    FragColor = vec4(rgb, 1.0);
}
