namespace ytLive.Services.Compositor;
///
/// Pure pixel math shared by the compositor: the WPF "UniformToFill" cover-crop
/// (what the preview's Stretch="UniformToFill" does) and a clamped bilinear
/// sample/scale. Pure and deterministic — the unit-tested half of the compositor.
///
public static class StretchMath
{
///
/// UniformToFill: the drawn content covers the destination while preserving the
/// source aspect, centered; overflow is cropped. Returns the scale and the
/// source-origin offset (in destination pixels) of the drawn content within the
/// destination rect.
///
public static (float Scale, float OffsetX, float OffsetY) UniformToFill(float dstW, float dstH, int srcW, int srcH)
{
var scale = Math.Max(dstW / srcW, dstH / srcH);
var drawnW = srcW * scale;
var drawnH = srcH * scale;
return (scale, (dstW - drawnW) / 2f, (dstH - drawnH) / 2f);
}
/// Clamped bilinear scale into a new tightly-packed BGRA8 frame; returns the input unchanged when the sizes already match.
public static VideoFrame BilinearScale(VideoFrame src, int outW, int outH)
{
if (outW == src.Width && outH == src.Height) return src;
var outPixels = new byte[outW * outH * 4];
for (var y = 0; y < outH; y++)
{
var sy = (y + 0.5f) / outH * src.Height - 0.5f;
for (var x = 0; x < outW; x++)
{
var sx = (x + 0.5f) / outW * src.Width - 0.5f;
var (b, g, r, a) = SampleBgra(src.BgraPixels, src.Width, src.Height, sx, sy);
var di = (y * outW + x) * 4;
outPixels[di] = b;
outPixels[di + 1] = g;
outPixels[di + 2] = r;
outPixels[di + 3] = a;
}
}
return new VideoFrame(outW, outH, outPixels);
}
///
/// Clamped bilinear sample of one BGRA8 pixel at fractional (sx, sy). Coordinates
/// outside [0, w-1]×[0, h-1] clamp to the edge, so a caller can sample freely
/// without bounds checks.
///
public static (byte B, byte G, byte R, byte A) SampleBgra(byte[] src, int w, int h, float sx, float sy)
{
sx = Math.Clamp(sx, 0, w - 1);
sy = Math.Clamp(sy, 0, h - 1);
var x0 = (int)sx;
var y0 = (int)sy;
var x1 = Math.Min(x0 + 1, w - 1);
var y1 = Math.Min(y0 + 1, h - 1);
var fx = sx - x0;
var fy = sy - y0;
var p00 = (y0 * w + x0) * 4;
var p10 = (y0 * w + x1) * 4;
var p01 = (y1 * w + x0) * 4;
var p11 = (y1 * w + x1) * 4;
float r = Lerp(Lerp(src[p00 + 2], src[p10 + 2], fx), Lerp(src[p01 + 2], src[p11 + 2], fx), fy);
float g = Lerp(Lerp(src[p00 + 1], src[p10 + 1], fx), Lerp(src[p01 + 1], src[p11 + 1], fx), fy);
float b = Lerp(Lerp(src[p00], src[p10], fx), Lerp(src[p01], src[p11], fx), fy);
float a = Lerp(Lerp(src[p00 + 3], src[p10 + 3], fx), Lerp(src[p01 + 3], src[p11 + 3], fx), fy);
return ((byte)Math.Round(b), (byte)Math.Round(g), (byte)Math.Round(r), (byte)Math.Round(a));
}
private static float Lerp(float a, float b, float t) => a + (b - a) * t;
}