using XFESystem.Desktop;
using XFESystem.Browser;
namespace XfePaintBaseline;
public static class BaselineBoxPainter
{
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);
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))));
});
}
// Solid background and border are composited independently so transparent
// borders retain the CSS background beneath them.
if (box.Background.Alpha > 0) DrawRows(coverage, native.Intersect(clip), box.Background.Rgb,
(x, y) => Coverage(native, radius, x, y) * 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) => Math.Clamp(.5 - Distance(bounds, radii, x, y), 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 DrawRows(IDesktopCoverageCanvas coverage, BrowserRect area, uint rgb, Func<double, double, double> sample)
{
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++)
{
bool any = false;
for (int x = left; x < right; 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, left, y, row.Length, 1, row);
}
}
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];
for(int y=top;y<bottom;y++)
{
for(int x=left;x<right;x++)
{ var color=BaselineGradientSample(gradient,x+.5-box.X,y+.5-box.Y,box.Width,box.Height); colors[x-left]=color.Rgb; row[x-left]=(byte)Math.Clamp(Math.Round(color.Alpha*opacity*Coverage(box,radius,x+.5,y+.5)*255),0,255); }
for(int start=0;start<row.Length;)
{ int end=start+1; while(end<row.Length&&colors[end]==colors[start])end++; coverage.BlendCoverage(colors[start],left+start,y,end-start,1,row.AsSpan(start,end-start));start=end; }
}
}
private static CssColor BaselineGradientSample(BrowserCssGradient gradient, double x, double y, double width, double height)
{
bool Radial=gradient.Radial; double CenterX=gradient.CenterX,CenterY=gradient.CenterY,Angle=gradient.Angle;var Stops=gradient.Stops;
double position;
if (Radial)
{
double cx=CenterX*width,cy=CenterY*height,rx=Math.Max(Math.Abs(cx),Math.Abs(width-cx)),ry=Math.Max(Math.Abs(cy),Math.Abs(height-cy));
double radius=Math.Sqrt(rx*rx+ry*ry); position=radius<=0?1:Math.Sqrt((x-cx)*(x-cx)+(y-cy)*(y-cy))/radius;
}
else
{
double radians=Angle*Math.PI/180,dx=Math.Sin(radians),dy=-Math.Cos(radians),length=Math.Abs(width*dx)+Math.Abs(height*dy);
position=length<=0?1:.5+((x-width/2)*dx+(y-height/2)*dy)/length;
}
if (position<=Stops[0].Position) return Stops[0].Color;
for(int i=1;i<Stops.Count;i++)
if(position<=Stops[i].Position)
{ double length=Stops[i].Position-Stops[i-1].Position; return Mix(Stops[i-1].Color,Stops[i].Color,length<=0?0:1-(position-Stops[i-1].Position)/length); }
return Stops[^1].Color;
}
private static CssColor Mix(CssColor a, CssColor b, double aWeight, double alphaMultiplier = 1)
{
double aa = a.Alpha * aWeight, ba = b.Alpha * (1 - aWeight), alpha = aa + ba;
if (alpha <= 0) return new(0, 0);
uint Channel(int shift) => (uint)Math.Clamp(Math.Round((((a.Rgb >> shift) & 255) * aa + ((b.Rgb >> shift) & 255) * ba) / alpha), 0, 255);
return new(Channel(16) << 16 | Channel(8) << 8 | Channel(0), alpha * alphaMultiplier);
}
}
using XFESystem.Desktop;
using XFESystem.Browser;
namespace XfePaintBaseline;
public static class BaselineBoxPainter
{
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);
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))));
});
}
// Solid background and border are composited independently so transparent
// borders retain the CSS background beneath them.
if (box.Background.Alpha > 0) DrawRows(coverage, native.Intersect(clip), box.Background.Rgb,
(x, y) => Coverage(native, radius, x, y) * 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) => Math.Clamp(.5 - Distance(bounds, radii, x, y), 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 DrawRows(IDesktopCoverageCanvas coverage, BrowserRect area, uint rgb, Func<double, double, double> sample)
{
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++)
{
bool any = false;
for (int x = left; x < right; 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, left, y, row.Length, 1, row);
}
}
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];
for(int y=top;y<bottom;y++)
{
for(int x=left;x<right;x++)
{ var color=BaselineGradientSample(gradient,x+.5-box.X,y+.5-box.Y,box.Width,box.Height); colors[x-left]=color.Rgb; row[x-left]=(byte)Math.Clamp(Math.Round(color.Alpha*opacity*Coverage(box,radius,x+.5,y+.5)*255),0,255); }
for(int start=0;start<row.Length;)
{ int end=start+1; while(end<row.Length&&colors[end]==colors[start])end++; coverage.BlendCoverage(colors[start],left+start,y,end-start,1,row.AsSpan(start,end-start));start=end; }
}
}
private static CssColor BaselineGradientSample(BrowserCssGradient gradient, double x, double y, double width, double height)
{
bool Radial=gradient.Radial; double CenterX=gradient.CenterX,CenterY=gradient.CenterY,Angle=gradient.Angle;var Stops=gradient.Stops;
double position;
if (Radial)
{
double cx=CenterX*width,cy=CenterY*height,rx=Math.Max(Math.Abs(cx),Math.Abs(width-cx)),ry=Math.Max(Math.Abs(cy),Math.Abs(height-cy));
double radius=Math.Sqrt(rx*rx+ry*ry); position=radius<=0?1:Math.Sqrt((x-cx)*(x-cx)+(y-cy)*(y-cy))/radius;
}
else
{
double radians=Angle*Math.PI/180,dx=Math.Sin(radians),dy=-Math.Cos(radians),length=Math.Abs(width*dx)+Math.Abs(height*dy);
position=length<=0?1:.5+((x-width/2)*dx+(y-height/2)*dy)/length;
}
if (position<=Stops[0].Position) return Stops[0].Color;
for(int i=1;i<Stops.Count;i++)
if(position<=Stops[i].Position)
{ double length=Stops[i].Position-Stops[i-1].Position; return Mix(Stops[i-1].Color,Stops[i].Color,length<=0?0:1-(position-Stops[i-1].Position)/length); }
return Stops[^1].Color;
}
private static CssColor Mix(CssColor a, CssColor b, double aWeight, double alphaMultiplier = 1)
{
double aa = a.Alpha * aWeight, ba = b.Alpha * (1 - aWeight), alpha = aa + ba;
if (alpha <= 0) return new(0, 0);
uint Channel(int shift) => (uint)Math.Clamp(Math.Round((((a.Rgb >> shift) & 255) * aa + ((b.Rgb >> shift) & 255) * ba) / alpha), 0, 255);
return new(Channel(16) << 16 | Channel(8) << 8 | Channel(0), alpha * alphaMultiplier);
}
}