#version 330 core
// Magnifier (local zoom lens) shader (docs-vn/rd/windows/plan/
// 2026-08-12-magnifier.md §2.1).
//
// Samples a BackdropCapture-produced texture -- GPU-native storage,
// same convention as mosaic_pixelate.fs / plate_passthrough.fs (row 0
// at the BOTTOM, hence the `1.0 - v` flip below) -- after pulling the
// sample position TOWARD the lens centre by u_zoomScale. This is Mac's
// renderMagnifier() pseudocode step 3 (§4.2): "sample = (uv - center) /
// zoomScale + center", the same magnification-by-inverse-sampling used
// by every GPU zoom-lens implementation.
//
// KEY INSIGHT (must stay in this comment, not just the plan): for
// u_zoomScale > 1, the sample position is pulled TOWARD the lens
// centre, so `uv01` always stays within [0,1] -- the capture rect
// never needs to be larger than the lens's own dst rect. This is the
// opposite of how a naive "zoom the source" shader would work (which
// would need to sample OUTSIDE [0,1] and therefore a wider capture).
//
// The shape cutout comes entirely from `u_maskTex` (MagnifierModel::
// maskSvgContent), same mask-texture convention as mosaic_pixelate.fs.
// Outside the mask, alpha=0 so the premultiplied SRC_OVER blend the
// caller uses is a no-op there -- the canvas content already drawn
// beneath this Magnifier layer shows through unchanged.
//
// Cover compositing (§2.3) is NOT in this shader -- it is a separate
// draw call layered around this one by the caller, same as how
// mosaic_pixelate.fs doesn't know about mosaic_shape_composite.fs.
//
// "zoom"-style border (§2.2, §4.4; Round/Square1 presets) IS in this
// shader: Mac's own recipe is "re-scale the already-masked lens shape
// by (borderWidth*2+w)/w, take the scaled shape's alpha as a solid-
// colour ring, composited BEHIND the original (unscaled) content" --
// literally "the same shape's mask sampled at a larger radius," which
// only needs one extra mask tap here, no separate draw call or new
// infrastructure. This is NOT a conventional stroke-along-the-edge
// algorithm; it deliberately produces the "magnifying-glass rim"
// double-image look Mac's own doc describes.
//
// GEOMETRY CONTRACT (must stay in sync with
// QuickCompositorBackend::compositeMagnifier and
// MagnifierKernel::magnifierZoomBorderScaleRatio): the mask texture
// itself already encodes the lens's "natural" (undecorated) size --
// Round's mask is r=400 in its own 1000-unit canvas, NOT r=500, i.e.
// the shape is intrinsically smaller than the full dst rect (confirmed
// 2026-08-18 against a real Mac .vn export). So the CONTENT CUTOUT term
// is sampled at `lensUv` (TexCoord un-rotated back into the lens's own
// frame, see the rotation-support uniforms below) with no additional
// scaling -- the mask's own geometry IS the natural content shape. The
// BORDER term samples the SAME mask at a SHRUNK `lensUv`
// (`(lensUv - 0.5) / u_borderScaleRatio + 0.5`) so the identical
// silhouette reads as larger on screen -- growing OUTWARD from the
// natural size toward the dst rect edge as border.width increases.
// Confirmed with the user 2026-08-18: only the lens's own SHAPE (this
// mask/border sampling) rotates with the layer -- the BACKDROP sample
// below (`u_backdrop`/`u_texScale`) deliberately stays on the RAW,
// un-rotated TexCoord, since rotating a lens must not rotate the canvas
// content shown through it. u_borderScaleRatio (MagnifierKernel::
// magnifierZoomBorderScaleRatio) is (maskWidth + borderWidth) /
// maskWidth computed entirely within the mask's own fixed 1000-unit
// authoring canvas -- independent of the lens's actual on-screen dst
// rect size -- growing smoothly across the UI's entire [0,200] slider
// range and self-limiting to exactly 1/kMagnifierNaturalSizeFraction
// (1.25) at border.width=200, the slider's own max, so the ring's outer
// edge reaches (but does not exceed) the dst rect right at the top of
// the range instead of saturating early. With no zoom border the caller
// passes ratio == 1.0 and the border term degenerates to sampling the
// same shape as content, i.e. a no-op ring.
//
// "sdf"-style border (Square2 preset) is a SEPARATE draw call using a
// baked SDF texture (see MagnifierSdfBorder.{h,cpp} once that lands) --
// intentionally NOT folded into this shader, because it needs an
// offline-generated distance-field texture this shader has no way to
// produce inline.
//
// Paired vertex shader: transition_dissolve.vs (plain passthrough --
// same as mosaic_pixelate.fs's own pairing).

out vec4 FragColor;
in  vec2 TexCoord;   // [0,1] over the dst-rect-sized viewport

uniform sampler2D u_backdrop;
// Fraction of the (possibly larger, never-shrink pooled) capture
// texture that actually holds this capture's valid pixels --
// vec2(dstW / poolW, dstH / poolH). Same never-shrink-scratch
// convention as mosaic_pixelate.fs's own u_texScale.
uniform vec2  u_texScale;
// Real zoom factor, range [1.0, 5.0] (MagnifierModel::zoomScale --
// the STORED value, never the UI slider position; see MagnifierKernel's
// realZoomScale()/displayZoomScale() for the shared conversion).
uniform float u_zoomScale;
// Layer opacity, [0,1] -- multiplied into the final alpha alongside the
// shape cutout.
uniform float u_opacity;
// Custom cross-platform SVG mask (MagnifierModel::maskSvgContent),
// rasterised the same way mosaic_pixelate.fs's u_maskTex is -- alpha
// channel only, sampled directly at TexCoord with NO flip: the source
// QImage is uploaded row-0-first, matching TexCoord.y==0 == this dst
// rect's top. u_useMaskTex is 0 only when the slice carries no mask at
// all, in which case the whole dst rect is filled uncropped.
uniform sampler2D u_maskTex;
uniform int   u_useMaskTex;
// "zoom"-style border (Round/Square1; Square2 uses "sdf" instead, see
// §4.4). u_useZoomBorder is 0 for every other case (no border at all,
// or an "sdf"-style border drawn by a separate pass) -- when 0, none
// of the uniforms below are read.
uniform int   u_useZoomBorder;
// Straight (non-premultiplied) RGBA border colour (MagnifierBorder::
// hexColor, 8-digit hex incl. alpha, decoded CPU-side).
uniform vec4  u_borderColor;
// Mac §4.4's scale ratio -- outer ring diameter over the mask's own
// natural diameter, precomputed CPU-side (MagnifierKernel::
// magnifierZoomBorderScaleRatio; this shader has no dst-rect-pixel
// uniform to derive it from inline, unlike mosaic_pixelate.fs's
// u_dstSizePx -- Magnifier doesn't need pixel-space math anywhere
// else, so there was nothing to reuse).
uniform float u_borderScaleRatio;

// Rotation support (mirrors yuv420p.fs's own applyOrient/u_rotRad/
// u_dstSize/u_unrotSize/u_rotPivot01/u_anchorPoint01 uniform set, bound
// via the SAME applyOrientationUniforms CPU-side helper): the caller
// (PreviewRenderer) expands the dst rect to the rotated AABB for any
// non-zero rotation, so TexCoord here spans [0,1] over that AABB, not
// over the lens's own unrotated square. Confirmed with the user
// 2026-08-18 that Magnifier must render rotation correctly (unlike
// Mosaic, which lost its rotate handle instead of getting this fix,
// since Mosaic shares the exact same backdrop-capture architecture and
// was equally broken). Defaults are no-op (u_rotRad=0), so every
// existing non-rotated call site renders byte-identical output.
uniform int   u_flipX;
uniform int   u_flipY;
uniform float u_rotRad;
uniform vec2  u_dstSize;
uniform vec2  u_unrotSize;
uniform vec2  u_rotPivot01;
uniform vec2  u_anchorPoint01;

vec2 applyOrient(vec2 uv) {
    if (u_rotRad != 0.0) {
        if (u_unrotSize.x > 0.0 && u_unrotSize.y > 0.0) {
            vec2 dPx = (uv - u_rotPivot01) * u_dstSize;
            float s = sin(-u_rotRad);
            float co = cos(-u_rotRad);
            vec2 sPx = vec2(co * dPx.x - s * dPx.y, s * dPx.x + co * dPx.y);
            uv = sPx / u_unrotSize + u_anchorPoint01;
        } else {
            vec2 c = uv - vec2(0.5);
            float s = sin(u_rotRad);
            float co = cos(u_rotRad);
            uv = vec2(co * c.x - s * c.y, s * c.x + co * c.y) + vec2(0.5);
        }
    }
    if (u_flipX != 0) uv.x = 1.0 - uv.x;
    if (u_flipY != 0) uv.y = 1.0 - uv.y;
    return uv;
}

void main() {
    // Un-rotate the AABB-space TexCoord back into the lens's own
    // unrotated [0,1] frame -- the MASK/SHAPE (what part of the canvas
    // is revealed, and the border ring) rotates WITH the lens, so those
    // are sampled at `lensUv` below. The BACKDROP itself must NOT
    // rotate -- looking through a tilted lens still shows the same,
    // undistorted room beneath, just through a differently-shaped hole
    // (confirmed with the user 2026-08-18: rotating a Magnifier must
    // only rotate the lens's own shape, not the canvas content sampled
    // through it) -- so the zoom-pull-toward-center sample uses the RAW
    // TexCoord, never lensUv.
    vec2 lensUv = applyOrient(TexCoord);

    vec2 uv01 = (TexCoord - 0.5) / u_zoomScale + 0.5;

    // `1.0 - uv01.y` flip MUST happen before `* u_texScale` -- capture
    // texture is FBO-native (row 0 at the bottom) and blit writes to
    // the pool's bottom-left corner; flipping after scaling would
    // sample the pool's empty top instead (same footgun mosaic_pixelate.fs
    // documents for its own identical line).
    vec2 uv = vec2(uv01.x, 1.0 - uv01.y) * u_texScale;
    vec4 color = texture(u_backdrop, uv);

    // Content is the mask's own natural shape -- no UV scaling needed,
    // see the GEOMETRY CONTRACT note above. Sampled at lensUv (not raw
    // TexCoord) so the mask cutout rotates WITH the lens.
    float shapeCutout = (u_useMaskTex == 1)
        ? texture(u_maskTex, lensUv).a
        : 1.0;

    // Straight (non-premultiplied) RGB + alpha, composited with a plain
    // src-over BEFORE the single premultiply at the very end -- keeps
    // the border/content blend a normal Porter-Duff `over` instead of
    // hand-rolled premultiplied algebra. The backdrop's own alpha
    // (color.a) folds into the content term here rather than scaling
    // the whole composite afterward -- the border, once added below, is
    // a fully opaque solid-colour ring independent of backdrop alpha.
    vec3 straightRgb = color.rgb;
    float straightA  = shapeCutout * color.a;

    if (u_useZoomBorder == 1) {
        // The border is the SAME shape, sampled at a SHRUNK uv so it
        // reads larger on screen -- growing outward from the natural
        // content size toward the dst rect edge. Where it is opaque
        // but `shapeCutout` is not, that ring is the border; the
        // masked content composites ON TOP of it (src-over), so the
        // border only shows outside the lens's own cutout.
        vec2 borderUv = (lensUv - 0.5) / u_borderScaleRatio + 0.5;
        float borderA = texture(u_maskTex, borderUv).a * u_borderColor.a;

        // content-over-border: standard Porter-Duff `over` with content
        // as the top (src) layer and the border ring as the bottom (dst).
        float outA = straightA + borderA * (1.0 - straightA);
        vec3  outRgb = (outA > 0.0)
            ? (straightRgb * straightA + u_borderColor.rgb * borderA * (1.0 - straightA)) / outA
            : vec3(0.0);
        straightRgb = outRgb;
        straightA   = outA;
    }

    float alpha = straightA * u_opacity;
    FragColor = vec4(straightRgb * alpha, alpha);
}
