DrawFiddle▶ RunEdit in Fiddle
// ── Before / After reveal ────────────────────────────────────────────────
// The same photo drawn twice, perfectly superposed: the raw frame, and the
// same frame run through an SkSL portrait filter. Both images use the SAME full-size layout,
// so they line up by construction — dragging the handle changes only the top
// image's CLIP rect, never any geometry. Nothing re-measures while you drag.

// The AFTER look: the portrait grade from the SKSL preset — 200mm perspective
// compression, shallow depth of field, beauty grading and film grain, all
// scaled by uIntensity (0 = untouched frame, 1 = full filter).
const string portraitLook = """
uniform float4 iMouse;           // Mouse drag pos=.xy Click pos=.zw (pixels)
uniform float  iTime;            // Shader playback time (s)
uniform float2 iResolution;      // Viewport resolution (pixels)
uniform float2 iImageResolution; // iImage1 resolution (pixels)
uniform shader iImage1;          // Texture
uniform float2 iOffset;          // Top-left corner of DrawingRect
uniform float2 iOrigin;          // Mouse drag started here
uniform float  uIntensity;       // 0 = original image, 1 = full filter


float2 telephotoCompression(float2 uv, float amount) {
    float2 centered = uv - 0.5;
    float distance = length(centered);

    // Compress TOWARDS center (pulls outer pixels inward - makes noses smaller!)
    // amount 0 -> compressionStrength 0 -> identity mapping (uv unchanged).
    float compressionStrength = 0.05 * amount;

    float compressionFactor = 1.0 + distance * compressionStrength;
    float2 compressed = centered / compressionFactor;

    // Calculate zoom needed to fill frame after compression
    // Maximum compression happens at corners (distance ~0.707)
    float maxDistance = 0.707; // sqrt(0.5^2 + 0.5^2)
    float maxCompressionFactor = 1.0 + maxDistance * compressionStrength;

    // Calculate zoom needed to fill frame after compression (cut in half)
    float zoomToFill = 1.0 + (maxCompressionFactor - 1.0) * 0.5; // Half way between no zoom and full zoom

    // Apply gentle zoom IN to partially fill black borders
    compressed /= zoomToFill;  // DIVIDE to zoom IN (magnify center area)

    return 0.5 + compressed;
}


half3 shallowDepthOfField(float2 coord, float2 uv, float amount) {

    // amount 0 -> blurRadius 0 -> plain sample (early-out below).
    float blurStrength = 0.65 * amount;


    float2 focusCenter = float2(0.5, 0.3);
    float distanceFromFocus = length(uv - focusCenter);

    float blurRadius = smoothstep(0.15, 0.45, distanceFromFocus) * blurStrength;

    if (blurRadius < 0.5) {
        return iImage1.eval(coord).rgb;
    }

    half3 blurred = half3(0.0);
    float totalWeight = 0.0;

    for (int i = -2; i <= 2; i++) {
        for (int j = -2; j <= 2; j++) {
            float2 offset = float2(float(i), float(j)) * blurRadius;
            float weight = exp(-dot(offset, offset) * 0.1);
            blurred += iImage1.eval(coord + offset).rgb * weight;
            totalWeight += weight;
        }
    }

    return blurred / totalWeight;
}


float subtleFilmGrain(float2 coord) {
    float2 grainCoord = coord * 0.8;
    float noise1 = fract(sin(dot(grainCoord, float2(127.1, 311.7))) * 43758.5453);
    float noise2 = fract(sin(dot(grainCoord * 1.3, float2(269.5, 183.3))) * 17951.3421);
    return (noise1 + noise2 - 1.0) * 0.015; // subtle
}


half3 portraitBeautyGrading(half3 color) {
    // Gentle contrast enhancement
    half3 enhanced = pow(color, half3(0.9, 0.9, 0.9));
    enhanced = (enhanced - 0.5) * 1.15 + 0.5;

    // Skin lift, hue-neutral: the preset raised r and g and left b behind,
    // which is exactly a yellow cast. Raise all three by the same gain and
    // the skin still glows without the tint.
    enhanced *= 1.02;

    // Gentle saturation boost — 1.2 pushed the yellow further, 1.08 keeps
    // the colour alive without amplifying the warm channels.
    float luminance = dot(enhanced, half3(0.299, 0.587, 0.114));
    enhanced = mix(half3(luminance), enhanced, 1.08);

    // Enhanced whites and soft highlight glow
    float brightness = (enhanced.r + enhanced.g + enhanced.b) / 3.0;
    float highlight = smoothstep(0.6, 1.0, brightness) * 0.08;
    enhanced += half3(highlight * 0.8, highlight * 0.9, highlight);

    // White enhancement - boost bright areas
    float whiteness = smoothstep(0.7, 0.95, brightness);
    enhanced = mix(enhanced, enhanced * 1.1, whiteness * 0.3);

    // Lift shadows slightly for softer look
    enhanced = max(enhanced + 0.02, 0.0) * 0.98 + 0.01;

    //reduce reds saturation
    enhanced = mix(enhanced, half3(dot(enhanced, half3(0.299,0.587,0.114))), clamp(enhanced.r - max(enhanced.g, enhanced.b), 0.0, 1.0) * 0.15);

    // reduce highlights 
    enhanced -= smoothstep(0.1, 1.75, dot(enhanced, half3(0.299, 0.587, 0.114))) * 0.25;

    // Final de-yellow: yellow is red+green with blue missing, so nudge blue
    // back up wherever it lags both of them. Skin keeps its warmth, the cast
    // goes. Raise 0.35 for cooler, lower for warmer.
    float yellowness = clamp(min(enhanced.r, enhanced.g) - enhanced.b, 0.0, 1.0);
    enhanced.b += yellowness * 0.1;

    return clamp(enhanced, 0.0, 1.0);
}


half4 main(float2 fragCoord) 
{	
    float2 renderingScale = iImageResolution.xy / iResolution.xy;
    float2 inputCoord = (fragCoord - iOffset) * renderingScale;
    float2 uv = inputCoord / iImageResolution.xy;

    // ===============================================
    // INJECTED LENS PROCESSING (APPLIED FIRST)
    // ===============================================

    // Apply 200mm perspective compression (scaled by intensity).
    float2 compressedUV = telephotoCompression(uv, uIntensity);
    float2 compressedCoord = compressedUV * iImageResolution.xy;

    // Get color with shallow depth of field (scaled by intensity).
    half3 baseColor = shallowDepthOfField(compressedCoord, compressedUV, uIntensity);

    // ===============================================
    // PORTRAIT BEAUTY PROCESSING
    // ===============================================

    // Color grade lerps back to ungraded base at intensity 0.
    half3 enhancedColor = mix(baseColor, portraitBeautyGrading(baseColor), uIntensity);

    // Film grain fades out at intensity 0.
    float grain = subtleFilmGrain(compressedCoord) * uIntensity;
    enhancedColor += half3(grain);

    enhancedColor = clamp(enhancedColor, 0.0, 1.0);

    return half4(enhancedColor, 1.0);
}
""";

const double Handle = 44;   // width of the draggable strip
const double Edge = 30;     // how close to the border the split may travel

SkiaImage after = null;     // the filtered copy, clipped to the right of the split
SkiaLayout divider = null;  // the strip you drag

double splitX = 100;         // split position in points, from the photo's left
double wide = 0;            // photo width in points

// Moves the divider and repaints the clipped image. That is the whole
// interaction: no layout pass, no cache rebuild, just a new clip rect.
void SetSplit(double x)
{
    if (wide <= 0)
        return;

    splitX = Math.Clamp(x, Edge, wide - Edge);
    divider.TranslationX = splitX - Handle / 2;
    after.Repaint();
}

// small pill caption over a corner of the photo
SkiaControl Pill(string text, bool right, string back, string ink) => new SkiaShape
{
    UseCache = SkiaCacheType.Image,
    Type = ShapeType.Rectangle,
    CornerRadius = 13,
    BackgroundColor = Color.Parse(back),
    Padding = new Thickness(11, 6),
    ZIndex = 3,
    HorizontalOptions = right ? LayoutOptions.End : LayoutOptions.Start,
    VerticalOptions = LayoutOptions.Start,
    Margin = right ? new Thickness(0, 14, 14, 0) : new Thickness(14, 14, 0, 0),
    Children = new List<SkiaControl>
    {
        new SkiaLabel(text)
        {
            FontSize = 11,
            CharacterSpacing = 3,
            TextColor = Color.Parse(ink),
        }
    }
};

return new SkiaLayer
{
    BackgroundColor = Color.Parse("#0B0B0D"),
    WidthRequest = 600, //limit max
    HorizontalOptions = LayoutOptions.Center,
    Children = new List<SkiaControl>
    {
        new SkiaStack
        {
            Spacing = 14,
            Padding = 18,
            VerticalOptions = LayoutOptions.Center,
            Children = new List<SkiaControl>
            {
                new SkiaLabel("Before / After")
                {
                    FontSize = 21,
                    TextColor = Colors.White,
                    HorizontalOptions = LayoutOptions.Center,
                },

                // the photo stage: rounded and clipped, so the images and the
                // divider can never paint outside the frame
                new SkiaShape
                {
                    Type = ShapeType.Rectangle,
                    CornerRadius = 18,
                    IsClippedToBounds = true,
                    HeightRequest = 340,
                    HorizontalOptions = LayoutOptions.Fill,
                    BackgroundColor = Color.Parse("#15161A"),
                    Children = new List<SkiaControl>
                    {
                        new SkiaLayer
                        {
                            HorizontalOptions = LayoutOptions.Fill,
                            VerticalOptions = LayoutOptions.Fill,
                            Children = new List<SkiaControl>
                            {
                                // BEFORE — the untouched frame
                                new SkiaImage
                                {
                                    Source = "cam.jpg",
                                    Aspect = TransformAspect.AspectCover,
                                    HorizontalOptions = LayoutOptions.Fill,
                                    VerticalOptions = LayoutOptions.Fill,
                                    // no cache: drawing a bitmap IS a blit, caching it
                                    // would only copy the same pixels into a second surface
                                    UseCache = SkiaCacheType.None,
                                },

                                // AFTER — same source, same layout, plus the SkSL grade.
                                // Only the clip differs, which is what makes the seam exact.
                                new SkiaImage
                                {
                                    Source = "cam.jpg",
                                    Aspect = TransformAspect.AspectCover,
                                    HorizontalOptions = LayoutOptions.Fill,
                                    VerticalOptions = LayoutOptions.Fill,
                                    // Cached, unlike BEFORE: a post-render shader needs the
                                    // control rasterised into a texture to sample. The photo
                                    // never changes, so it runs ONCE and the drag only
                                    // re-clips that surface.
                                    UseCache = SkiaCacheType.Image,
                                    LoadSourceOnFirstDraw = false,
                                    VisualEffects =
                                    {
                                        new SkiaShaderEffect
                                        {
                                            TileMode = SKShaderTileMode.Mirror,
                                            ShaderCode = portraitLook,
                                        }
                                        .SetUniform("uIntensity", 1f)
                                        // SkSL compile errors land here instead of being swallowed
                                        .OnShaderError((me, error) => Console.WriteLine($"[SkSL] {error}")),
                                    },
                                    // reveal everything to the RIGHT of the split
                                    Clipping = (path, dest) =>
                                    {
                                        path.Reset();
                                        var left = dest.Left + (float)(splitX * after.RenderingScale);
                                        path.AddRect(new SKRect(left, dest.Top, dest.Right, dest.Bottom));
                                    },
                                }.Assign(out after),

                                Pill("BEFORE", false, "#B3000000", "#C8CDD6"),
                                Pill("AFTER", true, "#E6000000", "#FFFFFF"),

                                // the draggable strip: a hairline plus a round grip.
                                // It is wider than the line so there is something to grab.
                                new SkiaLayout
                                {
                                    ZIndex = 5,
                                    WidthRequest = Handle,
                                    HorizontalOptions = LayoutOptions.Start,
                                    VerticalOptions = LayoutOptions.Fill,
                                    Children = new List<SkiaControl>
                                    {
                                        new SkiaShape
                                        {
                                            Type = ShapeType.Rectangle,
                                            BackgroundColor = Colors.White,
                                            WidthRequest = 2,
                                            HorizontalOptions = LayoutOptions.Center,
                                            VerticalOptions = LayoutOptions.Fill,
                                        },
                                        new SkiaShape
                                        {
                                            Type = ShapeType.Circle,
                                            BackgroundColor = Colors.White,
                                            WidthRequest = 38,
                                            LockRatio = 1,
                                            HorizontalOptions = LayoutOptions.Center,
                                            VerticalOptions = LayoutOptions.Center,
                                            Children = new List<SkiaControl>
                                            {
                                                // arrows as SVG: the bundled fonts have no ⇄ glyph
                                                new SkiaSvg
                                                {
                                                    SvgString = "<svg viewBox=\"0 0 24 24\"><path d=\"M7 9 L3 12 L7 15 M3 12 H13 M17 5 L21 8 L17 11 M21 8 H11\" fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>",
                                                    WidthRequest = 19,
                                                    LockRatio = 1,
                                                    HorizontalOptions = LayoutOptions.Center,
                                                    VerticalOptions = LayoutOptions.Center,
                                                }
                                            }
                                        },
                                    }
                                }
                                .Assign(out divider)
                                .WithGestures((me, args, apply) =>
                                {
                                    // pan moves the split by the finger delta; Down is
                                    // claimed so the drag keeps arriving here
                                    if (args.Type == TouchActionResult.Panning)
                                    {
                                        SetSplit(splitX + args.Event.Distance.Delta.X / me.RenderingScale);
                                        return me;
                                    }
                                    if (args.Type == TouchActionResult.Down)
                                        return me;
                                    return null;
                                }),
                            }
                        }
                        // the photo width is only known after layout, and it changes
                        // with the canvas, so the split is placed (and kept) from here
                        .ObserveSelf((me, prop) =>
                        {
                            if (prop != "Width" || me.Width <= 0)
                                return;
                            wide = me.Width;
                            SetSplit(splitX < 0 ? wide / 2 : splitX);
                        }),
                    }
                },

                new SkiaLabel("Drag the handle to compare")
                {
                    FontSize = 13,
                    TextColor = Color.Parse("#8A8F99"),
                    HorizontalOptions = LayoutOptions.Center,
                },
            }
        }
    }
};