namespace ytLive.Services.Compositor;
///
/// A region of the composed frame, blurred in place. The privacy layer (a Blur element on the
/// Live scene) and the webcam's background blur share this arithmetic, so "half strength" means
/// exactly the same blur in both places — the creator asked for consistency, and two copies of a
/// radius curve is how two features quietly disagree.
///
/// The algorithm is the webcam's: box-average downscale by
/// , three separable box passes at (≈ a Gaussian), then
/// blend back inside the mask. Working at ¼ resolution is what keeps a strong blur affordable.
///
/// Region-scoped. Cost is proportional to the blur's own rectangle, not the frame, so
/// a small privacy box costs almost nothing; the full-frame webcam case pays the full price.
///
public static class RegionBlur
{
/// Blur work happens at 1/ resolution and is upsampled while
/// blending — a strong blur at a fraction of the cost.
public const int Downscale = 4;
/// Box-blur radius (in downscaled pixels) at strength 0 and 1. The maximum was halved
/// from 12 on 2026-10-09: the creator found full range too heavy, and the established tools
/// blur far less (StreamFX defaults its box blur to 5).
public const int MinRadius = 1;
public const int MaxRadius = 6;
/// Strength (0–1) → box radius. Squared so the low half of the slider stays gentle.
public static int RadiusFor(double strength)
{
var s = double.IsFinite(strength) ? Math.Clamp(strength, 0, 1) : 0;
return (int)Math.Round(MinRadius + (MaxRadius - MinRadius) * s * s);
}
///
/// Blur a rectangle of (BGRA, bytes per row)
/// in place, inside a rectangular or elliptical mask.
///
///
/// Optional per-buffer-pixel map (1 byte each) of areas a STRONGER blur already covered. Pixels
/// marked there are skipped and never double-blurred, which is what makes overlapping blur
/// layers read as "the strongest one wins" instead of stacking into something stronger than any
/// single setting allows. The caller sorts blurs strongest-first and allocates this once.
///
/// True when it actually blurred something.
public static bool ApplyInPlace(
byte[] buffer, int stride, int frameWidth, int frameHeight,
int x, int y, int width, int height,
double strength, bool round, byte[]? coverage = null)
{
if (buffer is null || width <= 0 || height <= 0) return false;
// Clamp to the frame; the elements are draggable past the edges by design.
var x0 = Math.Clamp(x, 0, frameWidth);
var y0 = Math.Clamp(y, 0, frameHeight);
var x1 = Math.Clamp(x + width, 0, frameWidth);
var y1 = Math.Clamp(y + height, 0, frameHeight);
var w = x1 - x0;
var h = y1 - y0;
if (w <= 0 || h <= 0) return false;
var f = Downscale;
var sw = (w + f - 1) / f;
var sh = (h + f - 1) / f;
if (sw <= 0 || sh <= 0) return false;
var radius = RadiusFor(strength);
// 1. Downscale by box-averaging f×f blocks (3 channels; alpha is preserved as-is).
var small = new float[sw * sh * 3];
for (var sy = 0; sy < sh; sy++)
{
var py0 = y0 + sy * f;
var py1 = Math.Min(y0 + h, py0 + f);
for (var sx = 0; sx < sw; sx++)
{
var px0 = x0 + sx * f;
var px1 = Math.Min(x0 + w, px0 + f);
float b = 0, g = 0, r = 0;
var n = 0;
for (var py = py0; py < py1; py++)
{
var p = py * stride + px0 * 4;
for (var px = px0; px < px1; px++, p += 4)
{
b += buffer[p]; g += buffer[p + 1]; r += buffer[p + 2];
n++;
}
}
if (n == 0) n = 1;
var o = (sy * sw + sx) * 3;
small[o] = b / n; small[o + 1] = g / n; small[o + 2] = r / n;
}
}
// 2. Three separable box passes ≈ a Gaussian (the webcam's exact shape).
var scratch = new float[small.Length];
for (var pass = 0; pass < 3; pass++)
{
BoxHorizontal(small, scratch, sw, sh, radius);
BoxVertical(scratch, small, sw, sh, radius);
}
// 3. Blend back inside the mask. Bilinear on the small grid so the blur edge is not blocky.
var cx = w / 2.0;
var cy = h / 2.0;
var rx = Math.Max(1.0, w / 2.0);
var ry = Math.Max(1.0, h / 2.0);
for (var row = 0; row < h; row++)
{
var py = y0 + row;
var syf = (row + 0.5) / f - 0.5;
var sy0 = Math.Clamp((int)Math.Floor(syf), 0, sh - 1);
var sy1 = Math.Clamp(sy0 + 1, 0, sh - 1);
var ty = Math.Clamp(syf - sy0, 0, 1);
// Ellipse test in region space, per row: half-width of the chord.
var ny = (row + 0.5 - cy) / ry;
var chord = round && Math.Abs(ny) < 1 ? rx * Math.Sqrt(1 - ny * ny) : rx;
for (var col = 0; col < w; col++)
{
var px = x0 + col;
var index = py * stride + px * 4;
if (coverage is not null && coverage[py * frameWidth + px] != 0) continue;
if (round)
{
var dx = col + 0.5 - cx;
if (Math.Abs(dx) > chord) continue;
}
var sxf = (col + 0.5) / f - 0.5;
var sx0 = Math.Clamp((int)Math.Floor(sxf), 0, sw - 1);
var sx1 = Math.Clamp(sx0 + 1, 0, sw - 1);
var tx = Math.Clamp(sxf - sx0, 0, 1);
var i00 = (sy0 * sw + sx0) * 3;
var i01 = (sy0 * sw + sx1) * 3;
var i10 = (sy1 * sw + sx0) * 3;
var i11 = (sy1 * sw + sx1) * 3;
for (var c = 0; c < 3; c++)
{
var top = small[i00 + c] + (small[i01 + c] - small[i00 + c]) * tx;
var bottom = small[i10 + c] + (small[i11 + c] - small[i10 + c]) * tx;
var value = top + (bottom - top) * ty;
buffer[index + c] = ClampByte((float)value);
}
if (coverage is not null) coverage[py * frameWidth + px] = 1;
}
}
return true;
}
private static void BoxHorizontal(float[] src, float[] dst, int w, int h, int radius)
{
var window = radius * 2 + 1;
for (var y = 0; y < h; y++)
{
var row = y * w * 3;
for (var c = 0; c < 3; c++)
{
float sum = 0;
for (var i = -radius; i <= radius; i++)
sum += src[row + Math.Clamp(i, 0, w - 1) * 3 + c];
for (var x = 0; x < w; x++)
{
dst[row + x * 3 + c] = sum / window;
var add = Math.Clamp(x + radius + 1, 0, w - 1);
var drop = Math.Clamp(x - radius, 0, w - 1);
sum += src[row + add * 3 + c] - src[row + drop * 3 + c];
}
}
}
}
private static void BoxVertical(float[] src, float[] dst, int w, int h, int radius)
{
var window = radius * 2 + 1;
for (var x = 0; x < w; x++)
{
for (var c = 0; c < 3; c++)
{
float sum = 0;
for (var i = -radius; i <= radius; i++)
sum += src[Math.Clamp(i, 0, h - 1) * w * 3 + x * 3 + c];
for (var y = 0; y < h; y++)
{
dst[y * w * 3 + x * 3 + c] = sum / window;
var add = Math.Clamp(y + radius + 1, 0, h - 1);
var drop = Math.Clamp(y - radius, 0, h - 1);
sum += src[add * w * 3 + x * 3 + c] - src[drop * w * 3 + x * 3 + c];
}
}
}
}
private static byte ClampByte(float value) =>
value <= 0 ? (byte)0 : value >= 255 ? (byte)255 : (byte)value;
}