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; }