using XFESystem.Desktop;
namespace XFESystem.Browser;
public sealed record BrowserBoxShadow(double X, double Y, double Blur, double Spread, CssColor Color, bool Inset = false);
public sealed record BrowserBoxDecoration(BrowserEdges Radius, BrowserEdges Border, CssColor Background,
CssColor Top, CssColor Right, CssColor Bottom, CssColor Left, IReadOnlyList<BrowserBoxShadow> Shadows, BrowserRect Bounds,
IReadOnlyList<BrowserCssGradient> Gradients)
{
public BrowserRect PaintBounds
{
get
{
BrowserRect area = Bounds;
foreach (var shadow in Shadows.Where(shadow => !shadow.Inset))
{
double extent = shadow.Blur * 1.5 + shadow.Spread + 1;
area = area.Union(new(Bounds.X + shadow.X - extent, Bounds.Y + shadow.Y - extent, Math.Max(0, Bounds.Width + extent * 2), Math.Max(0, Bounds.Height + extent * 2)));
}
return area;
}
}
internal static BrowserBoxDecoration? Create(CssComputedStyle style, BrowserRect bounds, BrowserEdges border)
{
if (!style.Properties.ContainsKey("border-radius") && !style.Properties.ContainsKey("border-top-left-radius") && !style.Properties.ContainsKey("border-top-right-radius") &&
!style.Properties.ContainsKey("border-bottom-left-radius") && !style.Properties.ContainsKey("border-bottom-right-radius") && style.Get("box-shadow","none") == "none" && style.Get("background-image","none") == "none") return null;
string[] corners = ["top-left", "top-right", "bottom-right", "bottom-left"];
string[] tokens = CssValue.Split(style.Get("border-radius", "0"), ' ').ToArray();
if (tokens.Length is < 1 or > 4) tokens = ["0"];
string[] values = [tokens[0], tokens.Length > 1 ? tokens[1] : tokens[0], tokens.Length > 2 ? tokens[2] : tokens[0], tokens.Length > 3 ? tokens[3] : tokens.Length > 1 ? tokens[1] : tokens[0]];
var radii = new double[4];
for (int i = 0; i < 4; i++) radii[i] = Math.Max(0, CssValue.Length(style.Get("border-" + corners[i] + "-radius", values[i]), Math.Min(bounds.Width, bounds.Height), style.Font.Size, style.Viewport, 16) ?? 0);
double factor = 1;
void Limit(double available, double total) { if (total > 0) factor = Math.Min(factor, available / total); }
Limit(bounds.Width, radii[0] + radii[1]); Limit(bounds.Width, radii[3] + radii[2]);
Limit(bounds.Height, radii[0] + radii[3]); Limit(bounds.Height, radii[1] + radii[2]);
var shadows = new List<BrowserBoxShadow>();
foreach (string part in CssValue.Split(style.Get("box-shadow", "none"), ','))
{
var words = CssValue.Split(part.Trim(), ' ').ToArray(); var lengths = new List<double>(); CssColor color = style.Color; bool inset = false;
foreach (string word in words)
if (word == "inset") inset = true;
else if (CssValue.Length(word, 0, style.Font.Size, style.Viewport, 16) is { } length) lengths.Add(length);
else if (word != "none") color = CssValue.Color(word, style.Color);
if (lengths.Count is >= 2 and <= 4 && shadows.Count < 8)
shadows.Add(new(lengths[0], lengths[1], Math.Clamp(lengths.Count > 2 ? lengths[2] : 0, 0, 128), Math.Clamp(lengths.Count > 3 ? lengths[3] : 0, -128, 128), color, inset));
}
var gradients = BrowserCssGradient.Parse(style.Get("background-image", "none"), style,bounds);
if (radii.All(radius => radius == 0) && shadows.Count == 0 && gradients.Count == 0) return null;
CssColor Color(string side) => CssValue.Color(style.Get("border-" + side + "-color", "currentcolor"), style.Color);
return new(new(radii[0] * factor, radii[1] * factor, radii[2] * factor, radii[3] * factor), border, style.Background,
Color("top"), Color("right"), Color("bottom"), Color("left"), shadows.ToArray(), bounds, gradients);
}
}
/// <summary>Original device-pixel box rasterizer shared by the native and screenshot hosts.</summary>
public static class BrowserBoxRasterPainter
{
public static void Paint(IDesktopCanvas canvas, BrowserDrawCommand command, BrowserRect bounds, BrowserRect visible)
{
if (command.Decoration is not { } box) return;
if (canvas is not IDesktopCoverageCanvas coverage)
{ var area = box.Bounds.Translate(bounds.X-command.Bounds.X,bounds.Y-command.Bounds.Y).Intersect(visible); if(box.Background.Alpha>0)canvas.Fill(box.Background.Rgb,(int)area.X,(int)area.Y,(int)area.Width,(int)area.Height); return; }
double scale = canvas.ScalePercent / 100d;
BrowserRect actual = box.Bounds.Translate(bounds.X - command.Bounds.X, bounds.Y - command.Bounds.Y);
var native = new BrowserRect(actual.X * scale, actual.Y * scale, actual.Width * scale, actual.Height * scale);
var clip = new BrowserRect(visible.X * scale, visible.Y * scale, visible.Width * scale, visible.Height * scale);
var radius = Scale(box.Radius, scale); var border = Scale(box.Border, scale);
// Shadow samples under a later fully opaque background cannot affect
// the completed pixels. Stay inside a conservative straight-sided core;
// fractional edges and every ancestor rounded clip keep the old path.
BrowserRect? shadowOcclusion = null;
if (box.Background.Alpha == 1 && command.Opacity == 1 && canvas is not BrowserClipRasterCanvas)
{
double inset = Math.Max(.5, Math.Max(Math.Max(radius.Top, radius.Right), Math.Max(radius.Bottom, radius.Left)));
if (native.Width > inset * 2 && native.Height > 1)
shadowOcclusion = new(native.Left + inset, native.Top + .5, native.Width - inset * 2, native.Height - 1);
}
foreach (var shadow in box.Shadows.Reverse())
{
if (shadow.Inset) continue;
var rect = new BrowserRect(native.X + (shadow.X - shadow.Spread) * scale, native.Y + (shadow.Y - shadow.Spread) * scale,
Math.Max(0, native.Width + shadow.Spread * scale * 2), Math.Max(0, native.Height + shadow.Spread * scale * 2));
double blur = shadow.Blur * scale, extent = blur * 1.5;
var area = new BrowserRect(rect.X - extent - 1, rect.Y - extent - 1, rect.Width + extent * 2 + 2, rect.Height + extent * 2 + 2).Intersect(clip);
DrawRows(coverage, area, shadow.Color.Rgb, (x, y) =>
{
double distance = Distance(rect, radius, x, y);
return shadow.Color.Alpha * command.Opacity * (blur <= 0 ? Math.Clamp(.5 - distance, 0, 1) : .5 * (1 - Erf(distance / Math.Max(.5, blur / 2) / Math.Sqrt(2))));
}, shadowOcclusion);
}
// Solid background and border are composited independently so transparent
// borders retain the CSS background beneath them.
if (box.Background.Alpha > 0) DrawSolidRows(coverage, native.Intersect(clip), box.Background.Rgb,
native, radius, box.Background.Alpha, command.Opacity);
foreach (var gradient in box.Gradients.Reverse()) DrawGradientRows(coverage, native.Intersect(clip), native, radius, gradient, command.Opacity);
var inner = new BrowserRect(native.X + border.Left, native.Y + border.Top, Math.Max(0, native.Width - border.Horizontal), Math.Max(0, native.Height - border.Vertical));
var innerRadius = new BrowserEdges(Math.Max(0, radius.Top - Math.Max(border.Top, border.Left)), Math.Max(0, radius.Right - Math.Max(border.Top, border.Right)),
Math.Max(0, radius.Bottom - Math.Max(border.Bottom, border.Right)), Math.Max(0, radius.Left - Math.Max(border.Bottom, border.Left)));
if (border.Top + border.Right + border.Bottom + border.Left > 0)
foreach (int side in new[] { 0, 1, 2, 3 })
{
CssColor color = side switch { 0 => box.Top, 1 => box.Right, 2 => box.Bottom, _ => box.Left };
BrowserRect sideBounds = side switch
{
0 => new(native.X, native.Y, native.Width, Math.Max(border.Top, Math.Max(radius.Top, radius.Right))),
1 => new(native.Right - Math.Max(border.Right, Math.Max(radius.Right, radius.Bottom)), native.Y, Math.Max(border.Right, Math.Max(radius.Right, radius.Bottom)), native.Height),
2 => new(native.X, native.Bottom - Math.Max(border.Bottom, Math.Max(radius.Left, radius.Bottom)), native.Width, Math.Max(border.Bottom, Math.Max(radius.Left, radius.Bottom))),
_ => new(native.X, native.Y, Math.Max(border.Left, Math.Max(radius.Top, radius.Left)), native.Height)
};
DrawRows(coverage, sideBounds.Intersect(clip), color.Rgb, (x, y) =>
{
// Corner ownership follows the closest normalized border side.
double t = border.Top > 0 ? (y - native.Top) / border.Top : double.MaxValue;
double r = border.Right > 0 ? (native.Right - x) / border.Right : double.MaxValue;
double b = border.Bottom > 0 ? (native.Bottom - y) / border.Bottom : double.MaxValue;
double l = border.Left > 0 ? (x - native.Left) / border.Left : double.MaxValue;
double minimum = Math.Min(Math.Min(t, r), Math.Min(b, l));
int owner = t == minimum ? 0 : r == minimum ? 1 : b == minimum ? 2 : 3;
return owner != side ? 0 : Math.Max(0, Coverage(native, radius, x, y) - Coverage(inner, innerRadius, x, y)) * color.Alpha * command.Opacity;
});
}
}
private static BrowserEdges Scale(BrowserEdges value, double scale) => new(value.Top * scale, value.Right * scale, value.Bottom * scale, value.Left * scale);
public static double Coverage(BrowserRect bounds, BrowserEdges radii, double x, double y)
{
if (bounds.Width <= 0 || bounds.Height <= 0) return 0;
double radius = x < bounds.X + bounds.Width / 2 ? y < bounds.Y + bounds.Height / 2 ? radii.Top : radii.Left : y < bounds.Y + bounds.Height / 2 ? radii.Right : radii.Bottom;
double qx = Math.Abs(x - bounds.X - bounds.Width / 2) - bounds.Width / 2 + radius;
double qy = Math.Abs(y - bounds.Y - bounds.Height / 2) - bounds.Height / 2 + radius;
// Almost every box pixel belongs to a straight edge or its fully opaque
// interior. The expensive distance is needed only near a round corner.
if (qx <= 0 || qy <= 0)
return Math.Clamp(.5 - (Math.Max(qx, qy) - radius), 0, 1);
double squared = qx * qx + qy * qy;
if (radius >= .5 && squared < (radius - .5) * (radius - .5)) return 1;
if (squared > (radius + .5) * (radius + .5)) return 0;
return Math.Clamp(.5 - (Math.Sqrt(squared) - radius), 0, 1);
}
private static double Distance(BrowserRect rect, BrowserEdges radii, double x, double y)
{
if (rect.Width <= 0 || rect.Height <= 0) return double.MaxValue;
double radius = x < rect.X + rect.Width / 2 ? y < rect.Y + rect.Height / 2 ? radii.Top : radii.Left : y < rect.Y + rect.Height / 2 ? radii.Right : radii.Bottom;
double qx = Math.Abs(x - rect.X - rect.Width / 2) - rect.Width / 2 + radius;
double qy = Math.Abs(y - rect.Y - rect.Height / 2) - rect.Height / 2 + radius;
return Math.Sqrt(Math.Max(0, qx) * Math.Max(0, qx) + Math.Max(0, qy) * Math.Max(0, qy)) + Math.Min(Math.Max(qx, qy), 0) - radius;
}
private static double Erf(double x)
{
double sign = x < 0 ? -1 : 1; x = Math.Abs(x); double t = 1 / (1 + .3275911 * x);
return sign * (1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - .284496736) * t + .254829592) * t * Math.Exp(-x * x));
}
private static void DrawSolidRows(IDesktopCoverageCanvas coverage, BrowserRect area, uint rgb,
BrowserRect box, BrowserEdges radius, double alpha, double opacity)
{
int left=(int)Math.Floor(area.Left),right=(int)Math.Ceiling(area.Right),top=(int)Math.Floor(area.Top),bottom=(int)Math.Ceiling(area.Bottom);
if(right<=left||bottom<=top)return;
var row=new byte[right-left];
byte interior=(byte)Math.Clamp(Math.Round(alpha*opacity*255),0,255);
double largest=Math.Max(Math.Max(radius.Top,radius.Right),Math.Max(radius.Bottom,radius.Left));
for(int y=top;y<bottom;y++)
{
if(coverage is IDesktopRenderYieldCanvas yielding && yielding.RenderYieldRequested)throw new DesktopRenderYieldException();
// Only the guaranteed fully covered interior is constant. Fractional
// edges and rounded corners retain the original device-pixel formula.
int start=right,end=right;
if(y+.5>=box.Top+.5&&y+.5<=box.Bottom-.5)
{
bool middle=y+.5>=box.Top+largest&&y+.5<=box.Bottom-largest;
double inset=middle?0:largest;
start=Math.Clamp((int)Math.Ceiling(box.Left+inset),left,right);
end=Math.Clamp((int)Math.Floor(box.Right-inset),start,right);
}
bool any=false;
for(int x=left;x<start;x++) {row[x-left]=(byte)Math.Clamp(Math.Round(Coverage(box,radius,x+.5,y+.5)*alpha*opacity*255),0,255);any|=row[x-left]!=0;}
row.AsSpan(start-left,end-start).Fill(interior);any|=end>start&&interior!=0;
for(int x=end;x<right;x++) {row[x-left]=(byte)Math.Clamp(Math.Round(Coverage(box,radius,x+.5,y+.5)*alpha*opacity*255),0,255);any|=row[x-left]!=0;}
if(any)coverage.BlendCoverage(rgb,left,y,row.Length,1,row);
}
}
private static void DrawRows(IDesktopCoverageCanvas coverage, BrowserRect area, uint rgb, Func<double, double, double> sample,
BrowserRect? occluded = null)
{
int left = (int)Math.Floor(area.Left), right = (int)Math.Ceiling(area.Right), top = (int)Math.Floor(area.Top), bottom = (int)Math.Ceiling(area.Bottom);
if (right <= left || bottom <= top) return;
var row = new byte[right - left];
for (int y = top; y < bottom; y++)
{
if(coverage is IDesktopRenderYieldCanvas yielding && yielding.RenderYieldRequested)throw new DesktopRenderYieldException();
int skipStart = right, skipEnd = right;
if (occluded is { } hidden && y + .5 >= hidden.Top && y + .5 <= hidden.Bottom)
{
skipStart = (int)Math.Clamp(Math.Ceiling(hidden.Left - .5), left, right);
skipEnd = (int)Math.Clamp(Math.Floor(hidden.Right - .5) + 1, skipStart, right);
}
// Do not even submit the covered span. Besides avoiding sqrt/exp,
// this avoids walking an artificial zero-alpha mask in the driver.
for (int segment = 0; segment < 2; segment++)
{
int start = segment == 0 ? left : skipEnd, end = segment == 0 ? skipStart : right;
bool any = false;
for (int x = start; x < end; x++) { row[x - left] = (byte)Math.Clamp(Math.Round(sample(x + .5, y + .5) * 255), 0, 255); any |= row[x - left] != 0; }
if (any) coverage.BlendCoverage(rgb, start, y, end - start, 1, row.AsSpan(start - left, end - start));
}
}
}
private static void DrawGradientRows(IDesktopCoverageCanvas coverage, BrowserRect area, BrowserRect box, BrowserEdges radius, BrowserCssGradient gradient, double opacity)
{
int left=(int)Math.Floor(area.Left),right=(int)Math.Ceiling(area.Right),top=(int)Math.Floor(area.Top),bottom=(int)Math.Ceiling(area.Bottom);
if(right<=left||bottom<=top)return;
var row=new byte[right-left]; var colors=new uint[row.Length];
var sampler = gradient.Prepare(box.Width, box.Height);
for(int y=top;y<bottom;y++)
{
if(coverage is IDesktopRenderYieldCanvas yielding && yielding.RenderYieldRequested)throw new DesktopRenderYieldException();
bool any = false;
for(int x=left;x<right;x++)
{
var color=sampler.Sample(x+.5-box.X,y+.5-box.Y); colors[x-left]=color.Rgb;
// Preserve the original interpolation and nonzero-alpha
// arithmetic. A transparent sample needs no rounded coverage.
row[x-left] = color.Alpha == 0 || opacity == 0 ? (byte)0 :
(byte)Math.Clamp(Math.Round(color.Alpha*opacity*Coverage(box,radius,x+.5,y+.5)*255),0,255);
any |= row[x-left] != 0;
}
if (!any) continue;
for(int start=0;start<row.Length;)
{
int end=start+1; bool groupHasInk = row[start] != 0;
while(end<row.Length&&colors[end]==colors[start]) { groupHasInk |= row[end] != 0; end++; }
if (groupHasInk) coverage.BlendCoverage(colors[start],left+start,y,end-start,1,row.AsSpan(start,end-start));
start=end;
}
}
}
}
using XFESystem.Desktop;
namespace XFESystem.Browser;
public sealed record BrowserBoxShadow(double X, double Y, double Blur, double Spread, CssColor Color, bool Inset = false);
public sealed record BrowserBoxDecoration(BrowserEdges Radius, BrowserEdges Border, CssColor Background,
CssColor Top, CssColor Right, CssColor Bottom, CssColor Left, IReadOnlyList<BrowserBoxShadow> Shadows, BrowserRect Bounds,
IReadOnlyList<BrowserCssGradient> Gradients)
{
public BrowserRect PaintBounds
{
get
{
BrowserRect area = Bounds;
foreach (var shadow in Shadows.Where(shadow => !shadow.Inset))
{
double extent = shadow.Blur * 1.5 + shadow.Spread + 1;
area = area.Union(new(Bounds.X + shadow.X - extent, Bounds.Y + shadow.Y - extent, Math.Max(0, Bounds.Width + extent * 2), Math.Max(0, Bounds.Height + extent * 2)));
}
return area;
}
}
internal static BrowserBoxDecoration? Create(CssComputedStyle style, BrowserRect bounds, BrowserEdges border)
{
if (!style.Properties.ContainsKey("border-radius") && !style.Properties.ContainsKey("border-top-left-radius") && !style.Properties.ContainsKey("border-top-right-radius") &&
!style.Properties.ContainsKey("border-bottom-left-radius") && !style.Properties.ContainsKey("border-bottom-right-radius") && style.Get("box-shadow","none") == "none" && style.Get("background-image","none") == "none") return null;
string[] corners = ["top-left", "top-right", "bottom-right", "bottom-left"];
string[] tokens = CssValue.Split(style.Get("border-radius", "0"), ' ').ToArray();
if (tokens.Length is < 1 or > 4) tokens = ["0"];
string[] values = [tokens[0], tokens.Length > 1 ? tokens[1] : tokens[0], tokens.Length > 2 ? tokens[2] : tokens[0], tokens.Length > 3 ? tokens[3] : tokens.Length > 1 ? tokens[1] : tokens[0]];
var radii = new double[4];
for (int i = 0; i < 4; i++) radii[i] = Math.Max(0, CssValue.Length(style.Get("border-" + corners[i] + "-radius", values[i]), Math.Min(bounds.Width, bounds.Height), style.Font.Size, style.Viewport, 16) ?? 0);
double factor = 1;
void Limit(double available, double total) { if (total > 0) factor = Math.Min(factor, available / total); }
Limit(bounds.Width, radii[0] + radii[1]); Limit(bounds.Width, radii[3] + radii[2]);
Limit(bounds.Height, radii[0] + radii[3]); Limit(bounds.Height, radii[1] + radii[2]);
var shadows = new List<BrowserBoxShadow>();
foreach (string part in CssValue.Split(style.Get("box-shadow", "none"), ','))
{
var words = CssValue.Split(part.Trim(), ' ').ToArray(); var lengths = new List<double>(); CssColor color = style.Color; bool inset = false;
foreach (string word in words)
if (word == "inset") inset = true;
else if (CssValue.Length(word, 0, style.Font.Size, style.Viewport, 16) is { } length) lengths.Add(length);
else if (word != "none") color = CssValue.Color(word, style.Color);
if (lengths.Count is >= 2 and <= 4 && shadows.Count < 8)
shadows.Add(new(lengths[0], lengths[1], Math.Clamp(lengths.Count > 2 ? lengths[2] : 0, 0, 128), Math.Clamp(lengths.Count > 3 ? lengths[3] : 0, -128, 128), color, inset));
}
var gradients = BrowserCssGradient.Parse(style.Get("background-image", "none"), style,bounds);
if (radii.All(radius => radius == 0) && shadows.Count == 0 && gradients.Count == 0) return null;
CssColor Color(string side) => CssValue.Color(style.Get("border-" + side + "-color", "currentcolor"), style.Color);
return new(new(radii[0] * factor, radii[1] * factor, radii[2] * factor, radii[3] * factor), border, style.Background,
Color("top"), Color("right"), Color("bottom"), Color("left"), shadows.ToArray(), bounds, gradients);
}
}
/// <summary>Original device-pixel box rasterizer shared by the native and screenshot hosts.</summary>
public static class BrowserBoxRasterPainter
{
public static void Paint(IDesktopCanvas canvas, BrowserDrawCommand command, BrowserRect bounds, BrowserRect visible)
{
if (command.Decoration is not { } box) return;
if (canvas is not IDesktopCoverageCanvas coverage)
{ var area = box.Bounds.Translate(bounds.X-command.Bounds.X,bounds.Y-command.Bounds.Y).Intersect(visible); if(box.Background.Alpha>0)canvas.Fill(box.Background.Rgb,(int)area.X,(int)area.Y,(int)area.Width,(int)area.Height); return; }
double scale = canvas.ScalePercent / 100d;
BrowserRect actual = box.Bounds.Translate(bounds.X - command.Bounds.X, bounds.Y - command.Bounds.Y);
var native = new BrowserRect(actual.X * scale, actual.Y * scale, actual.Width * scale, actual.Height * scale);
var clip = new BrowserRect(visible.X * scale, visible.Y * scale, visible.Width * scale, visible.Height * scale);
var radius = Scale(box.Radius, scale); var border = Scale(box.Border, scale);
// Shadow samples under a later fully opaque background cannot affect
// the completed pixels. Stay inside a conservative straight-sided core;
// fractional edges and every ancestor rounded clip keep the old path.
BrowserRect? shadowOcclusion = null;
if (box.Background.Alpha == 1 && command.Opacity == 1 && canvas is not BrowserClipRasterCanvas)
{
double inset = Math.Max(.5, Math.Max(Math.Max(radius.Top, radius.Right), Math.Max(radius.Bottom, radius.Left)));
if (native.Width > inset * 2 && native.Height > 1)
shadowOcclusion = new(native.Left + inset, native.Top + .5, native.Width - inset * 2, native.Height - 1);
}
foreach (var shadow in box.Shadows.Reverse())
{
if (shadow.Inset) continue;
var rect = new BrowserRect(native.X + (shadow.X - shadow.Spread) * scale, native.Y + (shadow.Y - shadow.Spread) * scale,
Math.Max(0, native.Width + shadow.Spread * scale * 2), Math.Max(0, native.Height + shadow.Spread * scale * 2));
double blur = shadow.Blur * scale, extent = blur * 1.5;
var area = new BrowserRect(rect.X - extent - 1, rect.Y - extent - 1, rect.Width + extent * 2 + 2, rect.Height + extent * 2 + 2).Intersect(clip);
DrawRows(coverage, area, shadow.Color.Rgb, (x, y) =>
{
double distance = Distance(rect, radius, x, y);
return shadow.Color.Alpha * command.Opacity * (blur <= 0 ? Math.Clamp(.5 - distance, 0, 1) : .5 * (1 - Erf(distance / Math.Max(.5, blur / 2) / Math.Sqrt(2))));
}, shadowOcclusion);
}
// Solid background and border are composited independently so transparent
// borders retain the CSS background beneath them.
if (box.Background.Alpha > 0) DrawSolidRows(coverage, native.Intersect(clip), box.Background.Rgb,
native, radius, box.Background.Alpha, command.Opacity);
foreach (var gradient in box.Gradients.Reverse()) DrawGradientRows(coverage, native.Intersect(clip), native, radius, gradient, command.Opacity);
var inner = new BrowserRect(native.X + border.Left, native.Y + border.Top, Math.Max(0, native.Width - border.Horizontal), Math.Max(0, native.Height - border.Vertical));
var innerRadius = new BrowserEdges(Math.Max(0, radius.Top - Math.Max(border.Top, border.Left)), Math.Max(0, radius.Right - Math.Max(border.Top, border.Right)),
Math.Max(0, radius.Bottom - Math.Max(border.Bottom, border.Right)), Math.Max(0, radius.Left - Math.Max(border.Bottom, border.Left)));
if (border.Top + border.Right + border.Bottom + border.Left > 0)
foreach (int side in new[] { 0, 1, 2, 3 })
{
CssColor color = side switch { 0 => box.Top, 1 => box.Right, 2 => box.Bottom, _ => box.Left };
BrowserRect sideBounds = side switch
{
0 => new(native.X, native.Y, native.Width, Math.Max(border.Top, Math.Max(radius.Top, radius.Right))),
1 => new(native.Right - Math.Max(border.Right, Math.Max(radius.Right, radius.Bottom)), native.Y, Math.Max(border.Right, Math.Max(radius.Right, radius.Bottom)), native.Height),
2 => new(native.X, native.Bottom - Math.Max(border.Bottom, Math.Max(radius.Left, radius.Bottom)), native.Width, Math.Max(border.Bottom, Math.Max(radius.Left, radius.Bottom))),
_ => new(native.X, native.Y, Math.Max(border.Left, Math.Max(radius.Top, radius.Left)), native.Height)
};
DrawRows(coverage, sideBounds.Intersect(clip), color.Rgb, (x, y) =>
{
// Corner ownership follows the closest normalized border side.
double t = border.Top > 0 ? (y - native.Top) / border.Top : double.MaxValue;
double r = border.Right > 0 ? (native.Right - x) / border.Right : double.MaxValue;
double b = border.Bottom > 0 ? (native.Bottom - y) / border.Bottom : double.MaxValue;
double l = border.Left > 0 ? (x - native.Left) / border.Left : double.MaxValue;
double minimum = Math.Min(Math.Min(t, r), Math.Min(b, l));
int owner = t == minimum ? 0 : r == minimum ? 1 : b == minimum ? 2 : 3;
return owner != side ? 0 : Math.Max(0, Coverage(native, radius, x, y) - Coverage(inner, innerRadius, x, y)) * color.Alpha * command.Opacity;
});
}
}
private static BrowserEdges Scale(BrowserEdges value, double scale) => new(value.Top * scale, value.Right * scale, value.Bottom * scale, value.Left * scale);
public static double Coverage(BrowserRect bounds, BrowserEdges radii, double x, double y)
{
if (bounds.Width <= 0 || bounds.Height <= 0) return 0;
double radius = x < bounds.X + bounds.Width / 2 ? y < bounds.Y + bounds.Height / 2 ? radii.Top : radii.Left : y < bounds.Y + bounds.Height / 2 ? radii.Right : radii.Bottom;
double qx = Math.Abs(x - bounds.X - bounds.Width / 2) - bounds.Width / 2 + radius;
double qy = Math.Abs(y - bounds.Y - bounds.Height / 2) - bounds.Height / 2 + radius;
// Almost every box pixel belongs to a straight edge or its fully opaque
// interior. The expensive distance is needed only near a round corner.
if (qx <= 0 || qy <= 0)
return Math.Clamp(.5 - (Math.Max(qx, qy) - radius), 0, 1);
double squared = qx * qx + qy * qy;
if (radius >= .5 && squared < (radius - .5) * (radius - .5)) return 1;
if (squared > (radius + .5) * (radius + .5)) return 0;
return Math.Clamp(.5 - (Math.Sqrt(squared) - radius), 0, 1);
}
private static double Distance(BrowserRect rect, BrowserEdges radii, double x, double y)
{
if (rect.Width <= 0 || rect.Height <= 0) return double.MaxValue;
double radius = x < rect.X + rect.Width / 2 ? y < rect.Y + rect.Height / 2 ? radii.Top : radii.Left : y < rect.Y + rect.Height / 2 ? radii.Right : radii.Bottom;
double qx = Math.Abs(x - rect.X - rect.Width / 2) - rect.Width / 2 + radius;
double qy = Math.Abs(y - rect.Y - rect.Height / 2) - rect.Height / 2 + radius;
return Math.Sqrt(Math.Max(0, qx) * Math.Max(0, qx) + Math.Max(0, qy) * Math.Max(0, qy)) + Math.Min(Math.Max(qx, qy), 0) - radius;
}
private static double Erf(double x)
{
double sign = x < 0 ? -1 : 1; x = Math.Abs(x); double t = 1 / (1 + .3275911 * x);
return sign * (1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - .284496736) * t + .254829592) * t * Math.Exp(-x * x));
}
private static void DrawSolidRows(IDesktopCoverageCanvas coverage, BrowserRect area, uint rgb,
BrowserRect box, BrowserEdges radius, double alpha, double opacity)
{
int left=(int)Math.Floor(area.Left),right=(int)Math.Ceiling(area.Right),top=(int)Math.Floor(area.Top),bottom=(int)Math.Ceiling(area.Bottom);
if(right<=left||bottom<=top)return;
var row=new byte[right-left];
byte interior=(byte)Math.Clamp(Math.Round(alpha*opacity*255),0,255);
double largest=Math.Max(Math.Max(radius.Top,radius.Right),Math.Max(radius.Bottom,radius.Left));
for(int y=top;y<bottom;y++)
{
if(coverage is IDesktopRenderYieldCanvas yielding && yielding.RenderYieldRequested)throw new DesktopRenderYieldException();
// Only the guaranteed fully covered interior is constant. Fractional
// edges and rounded corners retain the original device-pixel formula.
int start=right,end=right;
if(y+.5>=box.Top+.5&&y+.5<=box.Bottom-.5)
{
bool middle=y+.5>=box.Top+largest&&y+.5<=box.Bottom-largest;
double inset=middle?0:largest;
start=Math.Clamp((int)Math.Ceiling(box.Left+inset),left,right);
end=Math.Clamp((int)Math.Floor(box.Right-inset),start,right);
}
bool any=false;
for(int x=left;x<start;x++) {row[x-left]=(byte)Math.Clamp(Math.Round(Coverage(box,radius,x+.5,y+.5)*alpha*opacity*255),0,255);any|=row[x-left]!=0;}
row.AsSpan(start-left,end-start).Fill(interior);any|=end>start&&interior!=0;
for(int x=end;x<right;x++) {row[x-left]=(byte)Math.Clamp(Math.Round(Coverage(box,radius,x+.5,y+.5)*alpha*opacity*255),0,255);any|=row[x-left]!=0;}
if(any)coverage.BlendCoverage(rgb,left,y,row.Length,1,row);
}
}
private static void DrawRows(IDesktopCoverageCanvas coverage, BrowserRect area, uint rgb, Func<double, double, double> sample,
BrowserRect? occluded = null)
{
int left = (int)Math.Floor(area.Left), right = (int)Math.Ceiling(area.Right), top = (int)Math.Floor(area.Top), bottom = (int)Math.Ceiling(area.Bottom);
if (right <= left || bottom <= top) return;
var row = new byte[right - left];
for (int y = top; y < bottom; y++)
{
if(coverage is IDesktopRenderYieldCanvas yielding && yielding.RenderYieldRequested)throw new DesktopRenderYieldException();
int skipStart = right, skipEnd = right;
if (occluded is { } hidden && y + .5 >= hidden.Top && y + .5 <= hidden.Bottom)
{
skipStart = (int)Math.Clamp(Math.Ceiling(hidden.Left - .5), left, right);
skipEnd = (int)Math.Clamp(Math.Floor(hidden.Right - .5) + 1, skipStart, right);
}
// Do not even submit the covered span. Besides avoiding sqrt/exp,
// this avoids walking an artificial zero-alpha mask in the driver.
for (int segment = 0; segment < 2; segment++)
{
int start = segment == 0 ? left : skipEnd, end = segment == 0 ? skipStart : right;
bool any = false;
for (int x = start; x < end; x++) { row[x - left] = (byte)Math.Clamp(Math.Round(sample(x + .5, y + .5) * 255), 0, 255); any |= row[x - left] != 0; }
if (any) coverage.BlendCoverage(rgb, start, y, end - start, 1, row.AsSpan(start - left, end - start));
}
}
}
private static void DrawGradientRows(IDesktopCoverageCanvas coverage, BrowserRect area, BrowserRect box, BrowserEdges radius, BrowserCssGradient gradient, double opacity)
{
int left=(int)Math.Floor(area.Left),right=(int)Math.Ceiling(area.Right),top=(int)Math.Floor(area.Top),bottom=(int)Math.Ceiling(area.Bottom);
if(right<=left||bottom<=top)return;
var row=new byte[right-left]; var colors=new uint[row.Length];
var sampler = gradient.Prepare(box.Width, box.Height);
for(int y=top;y<bottom;y++)
{
if(coverage is IDesktopRenderYieldCanvas yielding && yielding.RenderYieldRequested)throw new DesktopRenderYieldException();
bool any = false;
for(int x=left;x<right;x++)
{
var color=sampler.Sample(x+.5-box.X,y+.5-box.Y); colors[x-left]=color.Rgb;
// Preserve the original interpolation and nonzero-alpha
// arithmetic. A transparent sample needs no rounded coverage.
row[x-left] = color.Alpha == 0 || opacity == 0 ? (byte)0 :
(byte)Math.Clamp(Math.Round(color.Alpha*opacity*Coverage(box,radius,x+.5,y+.5)*255),0,255);
any |= row[x-left] != 0;
}
if (!any) continue;
for(int start=0;start<row.Length;)
{
int end=start+1; bool groupHasInk = row[start] != 0;
while(end<row.Length&&colors[end]==colors[start]) { groupHasInk |= row[end] != 0; end++; }
if (groupHasInk) coverage.BlendCoverage(colors[start],left+start,y,end-start,1,row.AsSpan(start,end-start));
start=end;
}
}
}
}