fix(web): CropBounds metadata + Fill-style scaling in compositor — transparent margins preserved, widget fills element rect
This commit is contained in:
@@ -420,9 +420,42 @@ public sealed class SceneCompositor
|
|||||||
VideoFrame src, float opacity, bool isRound, bool isMirror)
|
VideoFrame src, float opacity, bool isRound, bool isMirror)
|
||||||
{
|
{
|
||||||
if (ew <= 0 || eh <= 0) return;
|
if (ew <= 0 || eh <= 0) return;
|
||||||
var (scale, ox, oy) = StretchMath.UniformToFill(ew, eh, src.Width, src.Height);
|
|
||||||
var drawnW = src.Width * scale;
|
float scale, ox, oy;
|
||||||
var drawnH = src.Height * scale;
|
int srcCropX = 0, srcCropY = 0;
|
||||||
|
int w = src.Width, hgt = src.Height;
|
||||||
|
int sxMax, syMax;
|
||||||
|
float drawnW, drawnH;
|
||||||
|
|
||||||
|
if (src.CropBounds is (var cbX, var cbY, var cbW, var cbH))
|
||||||
|
{
|
||||||
|
// WebSource: Fill-style (stretch to cover, no aspect preservation) using
|
||||||
|
// the actual crop bounds. The transparent margins of the full canvas are
|
||||||
|
// preserved so they reveal layers beneath; only the crop content fills
|
||||||
|
// the element rect — matching the preview's Stretch="Fill" on the crop.
|
||||||
|
scale = Math.Max(ew / cbW, eh / cbH);
|
||||||
|
drawnW = cbW * scale;
|
||||||
|
drawnH = cbH * scale;
|
||||||
|
ox = (ew - drawnW) / 2f;
|
||||||
|
oy = (eh - drawnH) / 2f;
|
||||||
|
srcCropX = cbX;
|
||||||
|
srcCropY = cbY;
|
||||||
|
w = cbW;
|
||||||
|
hgt = cbH;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var (uScale, uOx, uOy) = StretchMath.UniformToFill(ew, eh, src.Width, src.Height);
|
||||||
|
scale = uScale;
|
||||||
|
ox = uOx;
|
||||||
|
oy = uOy;
|
||||||
|
drawnW = src.Width * scale;
|
||||||
|
drawnH = src.Height * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
sxMax = w - 1;
|
||||||
|
syMax = hgt - 1;
|
||||||
|
var invScale = 1f / scale;
|
||||||
var radius = Math.Min(ew, eh) / 2f;
|
var radius = Math.Min(ew, eh) / 2f;
|
||||||
var cx = ew / 2f;
|
var cx = ew / 2f;
|
||||||
var cy = eh / 2f;
|
var cy = eh / 2f;
|
||||||
@@ -433,16 +466,6 @@ public sealed class SceneCompositor
|
|||||||
var y1 = Math.Min(dstH - 1, (int)Math.Ceiling(ey + eh));
|
var y1 = Math.Min(dstH - 1, (int)Math.Ceiling(ey + eh));
|
||||||
if (x0 > x1 || y0 > y1) return;
|
if (x0 > x1 || y0 > y1) return;
|
||||||
|
|
||||||
// Integer fixed-point sampling + blend (libyuv's shape): the float
|
|
||||||
// SampleBgra + BlendPixel pair cost ~8 Math.Round + ~8 divisions per
|
|
||||||
// pixel and dominated this path in takes 3–4. Row invariants hoisted,
|
|
||||||
// the y-range check moved out of the x loop, and the dst alpha is the
|
|
||||||
// same opaque-master invariant as the row helpers (every write = 255).
|
|
||||||
var w = src.Width;
|
|
||||||
var hgt = src.Height;
|
|
||||||
var sxMax = w - 1;
|
|
||||||
var syMax = hgt - 1;
|
|
||||||
var invScale = 1f / scale;
|
|
||||||
var op = (int)(Math.Clamp(opacity, 0f, 1f) * 256);
|
var op = (int)(Math.Clamp(opacity, 0f, 1f) * 256);
|
||||||
if (op <= 0) return;
|
if (op <= 0) return;
|
||||||
var s = src.BgraPixels;
|
var s = src.BgraPixels;
|
||||||
@@ -451,7 +474,7 @@ public sealed class SceneCompositor
|
|||||||
{
|
{
|
||||||
var py = y - ey;
|
var py = y - ey;
|
||||||
if (py < oy || py > oy + drawnH) continue;
|
if (py < oy || py > oy + drawnH) continue;
|
||||||
var syRaw = Math.Clamp((py - oy) * invScale, 0f, syMax);
|
var syRaw = Math.Clamp((py - oy) * invScale, 0f, syMax) + srcCropY;
|
||||||
var syi = (int)syRaw;
|
var syi = (int)syRaw;
|
||||||
var sy1i = Math.Min(syi + 1, syMax);
|
var sy1i = Math.Min(syi + 1, syMax);
|
||||||
var fy8 = (int)((syRaw - syi) * 256);
|
var fy8 = (int)((syRaw - syi) * 256);
|
||||||
@@ -459,7 +482,7 @@ public sealed class SceneCompositor
|
|||||||
var di = (y * dstW + x0) * 4;
|
var di = (y * dstW + x0) * 4;
|
||||||
for (var x = x0; x <= x1; x++, di += 4)
|
for (var x = x0; x <= x1; x++, di += 4)
|
||||||
{
|
{
|
||||||
var px = x - ex; // element space
|
var px = x - ex;
|
||||||
if (px < ox || px > ox + drawnW) continue;
|
if (px < ox || px > ox + drawnW) continue;
|
||||||
if (isRound)
|
if (isRound)
|
||||||
{
|
{
|
||||||
@@ -468,7 +491,7 @@ public sealed class SceneCompositor
|
|||||||
if (dx * dx + dy * dy > radius * radius) continue;
|
if (dx * dx + dy * dy > radius * radius) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sxRaw = (px - ox) * invScale;
|
var sxRaw = Math.Clamp((px - ox) * invScale, 0f, sxMax) + srcCropX;
|
||||||
if (isMirror) sxRaw = sxMax - sxRaw;
|
if (isMirror) sxRaw = sxMax - sxRaw;
|
||||||
sxRaw = Math.Clamp(sxRaw, 0f, sxMax);
|
sxRaw = Math.Clamp(sxRaw, 0f, sxMax);
|
||||||
var sxi = (int)sxRaw;
|
var sxi = (int)sxRaw;
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ public sealed class VideoFrame
|
|||||||
/// be transparent (static art, web overlays, chat, the social-bar strip).</summary>
|
/// be transparent (static art, web overlays, chat, the social-bar strip).</summary>
|
||||||
public bool IsOpaque { get; init; }
|
public bool IsOpaque { get; init; }
|
||||||
|
|
||||||
|
/// <summary>WebSource crop metadata: the alpha bounding box of the widget content
|
||||||
|
/// within the full canvas (X/Y/W/H in pixels). When set, the compositor uses these
|
||||||
|
/// bounds for Fill-style scaling (stretch to cover, no aspect preservation) instead
|
||||||
|
/// of UniformToFill. The transparent margins of the full canvas are preserved so
|
||||||
|
/// they reveal layers beneath — the crop defines the actual content region.</summary>
|
||||||
|
public (int X, int Y, int W, int H)? CropBounds { get; init; }
|
||||||
|
|
||||||
public VideoFrame(int width, int height, byte[] bgraPixels)
|
public VideoFrame(int width, int height, byte[] bgraPixels)
|
||||||
{
|
{
|
||||||
Width = width;
|
Width = width;
|
||||||
|
|||||||
@@ -270,7 +270,11 @@ public sealed class WebView2Manager : IDisposable
|
|||||||
session.PreviewBitmap = new WriteableBitmap(cropW, cropH, 96, 96, PixelFormats.Bgra32, null);
|
session.PreviewBitmap = new WriteableBitmap(cropW, cropH, 96, 96, PixelFormats.Bgra32, null);
|
||||||
session.PreviewBitmap.WritePixels(new Int32Rect(0, 0, cropW, cropH), outPixels, cropW * 4, 0);
|
session.PreviewBitmap.WritePixels(new Int32Rect(0, 0, cropW, cropH), outPixels, cropW * 4, 0);
|
||||||
|
|
||||||
session.LatestFrame = new VideoFrame(pixW, pixH, pixels) { Epoch = ++session.Epoch };
|
session.LatestFrame = new VideoFrame(pixW, pixH, pixels)
|
||||||
|
{
|
||||||
|
Epoch = ++session.Epoch,
|
||||||
|
CropBounds = (cropX, cropY, cropW, cropH),
|
||||||
|
};
|
||||||
|
|
||||||
// The preview bitmap is now MUTABLE and reused (CameraManager precedent):
|
// The preview bitmap is now MUTABLE and reused (CameraManager precedent):
|
||||||
// WPF re-renders it after WritePixels; handlers must not Freeze it.
|
// WPF re-renders it after WritePixels; handlers must not Freeze it.
|
||||||
|
|||||||
Reference in New Issue
Block a user