DrawFiddle▶ RunEdit in Fiddle
// ── Filters Camera: before / after ───────────────────────────────────────
// Widget premium-published and used on the 
// Filters Camera landing page inside an <iframe>

const PHOTO = "https://filterscamera.app/sample.jpg";
const MONO = "https://filterscamera.app/fonts/ibm-plex-mono-400.woff2";   // the site's tag font
const HANDLE = 44;   // knob size, also the width of the strip you grab
const EDGE = 22;     // how close to the border the split may travel

// ↔ drawn as a path centred on the viewBox (y 8..16 around 12), so it sits in the middle
// of the knob. The bundled text fonts have no arrow glyph.
const ARROWS = `<svg viewBox="0 0 24 24"><path d="M3 12 H21 M7 8 L3 12 L7 16 M17 8 L21 12 L17 16" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;

const TAG_PADDING = new Thickness(9, 4);
const TAG_LEFT = new Thickness(14, 14, 0, 0);
const TAG_RIGHT = new Thickness(0, 14, 14, 0);

// The site's CSS filter functions as colour matrices, same maths as the spec:
// saturate() mixes towards Rec.709 luminance, contrast() pivots on 0.5, brightness() scales.
function cssMatrix(CK) {
  const matrix = (m) => CK.ColorFilter.MakeMatrix(m);
  const saturate = (s) => matrix([
    0.2126 + 0.7874 * s, 0.7152 - 0.7152 * s, 0.0722 - 0.0722 * s, 0, 0,
    0.2126 - 0.2126 * s, 0.7152 + 0.2848 * s, 0.0722 - 0.0722 * s, 0, 0,
    0.2126 - 0.2126 * s, 0.7152 - 0.7152 * s, 0.0722 + 0.9278 * s, 0, 0,
    0, 0, 0, 1, 0]);
  const contrast = (c) => matrix([c, 0, 0, 0, 0.5 - 0.5 * c, 0, c, 0, 0, 0.5 - 0.5 * c, 0, 0, c, 0, 0.5 - 0.5 * c, 0, 0, 0, 1, 0]);
  const brightness = (b) => matrix([b, 0, 0, 0, 0, 0, b, 0, 0, 0, 0, 0, b, 0, 0, 0, 0, 0, 1, 0]);
  // CSS applies a filter list left to right; MakeCompose(outer, inner) runs inner first
  const chain = (...filters) => filters.reduce((inner, outer) => CK.ColorFilter.MakeCompose(outer, inner));
  return {
    before: chain(saturate(0.68), contrast(0.9), brightness(1.02)),   // .split .before
    after: chain(saturate(1.65), contrast(1.12)),                      // .split .after
  };
}

// small caption in a top corner
function Tag({ text, right, ink, font }) {
  return (
    <SkiaShape Type="Rectangle" CornerRadius={5} BackgroundColor="#8C080808" UseCache="Image"
               Padding={TAG_PADDING} ZIndex={5}
               HorizontalOptions={right ? "End" : "Start"} VerticalOptions="Start"
               Margin={right ? TAG_RIGHT : TAG_LEFT}>
      {/* the site spaces these letters by .16em: CharacterSpacing is not ported to DrawnUi.React yet */}
      <SkiaLabel Text={text} FontFamily={font} FontSize={9} TextColor={ink} />
    </SkiaShape>
  );
}

// hold the embed's "ready" message until the photos are on screen (released in App)
Fiddle.HoldReady();

export default function App() {
  const stage = useRef(null);    // the whole canvas: its width bounds the split
  const reveal = useRef(null);   // clipped window onto AFTER, slid to the split
  const after = useRef(null);    // the graded copy, slid back so its pixels stay put
  const divider = useRef(null);  // line + knob
  const ratio = useRef(0.5);     // split as a fraction of the width, so a resize keeps it in place
  const [font, setFont] = useState("FontText");

  // The page that embeds this keeps its static poster until DrawFiddle posts "ready": held at
  // module load (above App), released once both photos have loaded, so the reveal never shows
  // the canvas without its picture.
  const loaded = useRef(0);
  const onImage = () => { if (++loaded.current === 2) Fiddle.Ready(); };

  // Colour filters, not shaders: one matrix per pixel at draw time, as cheap as the blit itself.
  const filters = useMemo(() => cssMatrix(Core.Super.CK), []);

  // The tag font is the site's own woff2, registered once under its own alias; the tags
  // re-measure when it lands and show the default face until then.
  useEffect(() => {
    let alive = true;
    fetch(MONO).then((r) => r.arrayBuffer()).then((data) => {
      const face = Core.Super.CK.Typeface.MakeFreeTypeFaceFromData(data);
      if (!face || !alive) return;
      Core.Super.Fonts.set("FontMono", new Map([[400, face]]));
      setFont("FontMono");
    }).catch(() => {});
    return () => { alive = false; };
  }, []);

  // DrawnUi.React has no Clipping callback yet, so the reveal is a clipped layer slid right by
  // the split with the image inside slid left by the same amount: the clip travels, the pixels
  // stay. Field writes + RepaintComposition: no React render and no layout pass per drag frame.
  const place = (x) => {
    const box = stage.current;
    const wide = box ? box.DrawingRect.Width / box.RenderingScale : 0;
    if (wide <= 0) return;
    const split = Math.min(Math.max(x, EDGE), wide - EDGE);
    ratio.current = split / wide;
    reveal.current.TranslationX = split;
    after.current.TranslationX = -split;
    divider.current.TranslationX = split - HANDLE / 2;
    reveal.current.RepaintComposition();
    divider.current.RepaintComposition();
  };

  // Place the split once the width is known, and again only when it really changes: the hook
  // runs on every arrange, and re-placing an unchanged split would request a frame whose
  // arrange calls the hook again — a loop that never lets the canvas go idle.
  useEffect(() => {
    const box = stage.current;
    const base = box.OnLayoutChanged.bind(box);
    let lastWide = -1;
    box.OnLayoutChanged = () => {
      base();
      const wide = box.DrawingRect.Width / box.RenderingScale;
      if (wide === lastWide) return;
      lastWide = wide;
      place(ratio.current * wide);
    };
    box.OnLayoutChanged();   // the first layout may already be done by now
  }, []);

  return (
    <SkiaLayer ref={stage}
     AccessibilityRole={Aria.RoleButton}
     HorizontalOptions = "Center"
     WidthRequest = {575}
     HeightRequest = {719}
     BackgroundColor="#000000">

      {/* BEFORE — "usual camera app": saturate(.68) contrast(.9) brightness(1.02) */}
      <SkiaImage Success={onImage} Source={PHOTO} Aspect="AspectCover" AddEffect="Custom" PaintColorFilter={filters.before}
                 HorizontalOptions="Fill" VerticalOptions="Fill" />

      {/* AFTER — "Filters Camera": saturate(1.65) contrast(1.12), revealed right of the split */}
      <SkiaLayer ref={reveal} IsClippedToBounds HorizontalOptions="Fill" VerticalOptions="Fill">
        <SkiaImage Success={onImage} ref={after} Source={PHOTO} Aspect="AspectCover" AddEffect="Custom" PaintColorFilter={filters.after}
                   HorizontalOptions="Fill" VerticalOptions="Fill" />
      </SkiaLayer>

      <Tag text="PHONE CAMERA" right={false} ink="#8E8A84" font={font} />
      <Tag text="ROMANCE" right ink="#D9ECEAE6" font={font} />

      {/* the handle: a 2px line and a round knob, the whole strip is draggable */}
      <SkiaLayer ref={divider} ZIndex={4} WidthRequest={HANDLE}
                 HorizontalOptions="Start" VerticalOptions="Fill"
                 ConsumeGestures={(me, e) => {
                   // Down is claimed so the drag keeps arriving here even off the strip.
                   // Delta is pixels; the split is points.
                   const type = e.Args.Type;
                   if (type === "Down") {
                     e.Consumed = true;
                   } else if (type === "Panning") {
                     const box = stage.current;
                     place(ratio.current * box.DrawingRect.Width / box.RenderingScale + e.Args.Event.Distance.Delta.X / me.RenderingScale);
                     e.Consumed = true;
                   }
                 }}>
        <SkiaShape Type="Rectangle" BackgroundColor="#E6ECEAE6" WidthRequest={2}
                   HorizontalOptions="Center" VerticalOptions="Fill" />
        <SkiaShape Type="Circle" BackgroundColor="#ECEAE6" WidthRequest={HANDLE} LockRatio={1}
                   HorizontalOptions="Center" VerticalOptions="Center">
          <SkiaSvg SvgString={ARROWS} WidthRequest={16} LockRatio={1}
                   HorizontalOptions="Center" VerticalOptions="Center" />
        </SkiaShape>
      </SkiaLayer>
    </SkiaLayer>
  );
}