Zoom: anchor the capture, magnify the full composite, draw on top (Unit: zoom rework per creator correction)
Creator corrected the "loupe" zoom on 2026-10-08: the click anchors a fixed
capture region (box/2 centred on the click) and the box is a live 2x view of
that region — dragging the box never changes its content; it magnifies the
fully-composited frame (every layer, incl. webcam) and draws LAST (n+1).
Rectangle shape is 4:3 ("more of a 640x480 aspect").
- MainViewModel.Zoom.cs rewritten: ActivateZoom anchors CaptureX/CaptureY
(independently clamped); UpdateZoomMove relocates the box only; the per-frame
BuildZoomFrame/ZoomFrameBuilder path is gone.
- SceneCompositor: skips the active ZoomSceneElement in every layer loop and
BlitZooms it last via the new pure ZoomMagnifier (2x bilinear, temp snapshot
via ArrayPool, round clip).
- Preview: ZoomBox hoisted above CanvasGrid as a sibling; its brush samples the
whole composed preview at the fixed capture (2x).
- Tests: ZoomMagnifierTests (box-over-blue shows green capture at 2x incl. an
extra red "layer" — the loupe/backdrop-only regressions both fail it) +
round-clip; ZoomBoxInteractionTests rewritten (anchored capture, move-doesn't-
change-capture, 4:3/9:16/square). 404 pass, 2 known environment flakes.
This commit is contained in:
@@ -157,6 +157,7 @@ public sealed class SceneCompositor
|
||||
case Source { IsBackground: true }:
|
||||
case Source { Type: SourceType.Background }:
|
||||
case Source { Type: SourceType.TextOverlay }:
|
||||
case ZoomSceneElement { IsZoomActive: true }:
|
||||
continue;
|
||||
}
|
||||
var fullFrame = frameFor(element);
|
||||
@@ -175,6 +176,7 @@ public sealed class SceneCompositor
|
||||
case Source { IsBackground: true }:
|
||||
case Source { Type: SourceType.Background }:
|
||||
case Source { Type: SourceType.TextOverlay }:
|
||||
case ZoomSceneElement { IsZoomActive: true }:
|
||||
continue;
|
||||
}
|
||||
var frame = frameFor(element);
|
||||
@@ -191,6 +193,14 @@ public sealed class SceneCompositor
|
||||
if (tickerFrame != null && tickerFrame.Width > 0 && tickerFrame.Height > 0)
|
||||
BlitOverlay(buffer, cropW, cropH, options, tickerFrame, tickerFrame.OriginX, tickerFrame.OriginY);
|
||||
|
||||
// The zoom box is layer n+1: a live 2x view of a FIXED capture window sampled
|
||||
// from the frame just composited (all layers). Drawn LAST — above everything.
|
||||
// The box/2 capture is anchored at the click, so dragging the box only moves
|
||||
// the box; the content it shows never changes (creator ruling 2026-10-08).
|
||||
var zoom = scene.Elements.OfType<ZoomSceneElement>().FirstOrDefault(z => z.IsZoomActive);
|
||||
if (zoom != null)
|
||||
BlitZoom(buffer, cropW, cropH, options, zoom);
|
||||
|
||||
return StretchMath.BilinearScale(
|
||||
new VideoFrame(cropW, cropH, buffer), options.OutputWidth, options.OutputHeight);
|
||||
}
|
||||
@@ -249,6 +259,7 @@ public sealed class SceneCompositor
|
||||
case Source { IsBackground: true }:
|
||||
case Source { Type: SourceType.Background }:
|
||||
case Source { Type: SourceType.TextOverlay }:
|
||||
case ZoomSceneElement:
|
||||
continue;
|
||||
}
|
||||
var frame = frameFor(element);
|
||||
@@ -299,6 +310,7 @@ public sealed class SceneCompositor
|
||||
case Source { IsBackground: true }:
|
||||
case Source { Type: SourceType.Background }:
|
||||
case Source { Type: SourceType.TextOverlay }:
|
||||
case ZoomSceneElement { IsZoomActive: true }:
|
||||
continue;
|
||||
}
|
||||
var frame = frameFor(element);
|
||||
@@ -309,6 +321,10 @@ public sealed class SceneCompositor
|
||||
if (tickerFrame != null && tickerFrame.Width > 0 && tickerFrame.Height > 0)
|
||||
BlitOverlay(buffer, cropW, cropH, options, tickerFrame, tickerFrame.OriginX, tickerFrame.OriginY);
|
||||
|
||||
var zoom = scene.Elements.OfType<ZoomSceneElement>().FirstOrDefault(z => z.IsZoomActive);
|
||||
if (zoom != null)
|
||||
BlitZoom(buffer, cropW, cropH, options, zoom);
|
||||
|
||||
return StretchMath.BilinearScale(new VideoFrame(cropW, cropH, buffer), options.OutputWidth, options.OutputHeight);
|
||||
}
|
||||
|
||||
@@ -834,6 +850,23 @@ public sealed class SceneCompositor
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The zoom box layer (n+1): map the element's master-space box and capture to
|
||||
/// crop space and 2x-magnify the capture onto the box from the composed frame.
|
||||
/// The active zoom element is never blitted by the normal layer loops — the
|
||||
/// switch skips it and this pass draws it above everything, last.
|
||||
/// </summary>
|
||||
private static void BlitZoom(
|
||||
byte[] dst, int cropW, int cropH, CompositorOptions options, ZoomSceneElement zoom)
|
||||
{
|
||||
ZoomMagnifier.MagnifyInto(
|
||||
dst, cropW, cropH,
|
||||
zoom.X - options.SourceRectX, zoom.Y - options.SourceRectY,
|
||||
zoom.Width, zoom.Height,
|
||||
zoom.CaptureX - options.SourceRectX, zoom.CaptureY - options.SourceRectY,
|
||||
zoom.ClipShape == ClipShape.Round);
|
||||
}
|
||||
|
||||
/// <summary>Straight-alpha source-over blend; the frame's alpha (and the layer opacity) drives coverage.</summary>
|
||||
private static void BlendPixel(byte[] dst, int di, (byte B, byte G, byte R, byte A) src, float opacity)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user