namespace XFESystem.Browser;
public sealed record BrowserGradientStop(CssColor Color, double Position);
/// <summary>Finite, non-repeating CSS linear and circular radial gradient layers.</summary>
public sealed record BrowserCssGradient(bool Radial, double Angle, double CenterX, double CenterY, IReadOnlyList<BrowserGradientStop> Stops)
{
internal static IReadOnlyList<BrowserCssGradient> Parse(string text, CssComputedStyle style, BrowserRect? box = null)
{
if (!text.Contains("gradient(",StringComparison.OrdinalIgnoreCase)) return [];
var result = new List<BrowserCssGradient>();
foreach (string raw in CssValue.Split(text, ','))
{
string layer = raw.Trim(); bool radial = layer.StartsWith("radial-gradient(", StringComparison.OrdinalIgnoreCase);
int open = layer.IndexOf('(');
if ((!radial && !layer.StartsWith("linear-gradient(", StringComparison.OrdinalIgnoreCase)) || !layer.EndsWith(')') || open < 0 || result.Count >= 8) continue;
string[] parts = CssValue.Split(layer[(open + 1)..^1], ',').Select(part => part.Trim()).ToArray();
if (parts.Length < 2 || parts.Length > 64) continue;
double angle = 180, centerX = .5, centerY = .5; int start = 0;
double width=box?.Width??style.Viewport.Width,height=box?.Height??style.Viewport.Height;
if (!radial && parts[0].EndsWith("deg", StringComparison.OrdinalIgnoreCase)) { angle = CssValue.Number(parts[0][..^3], 180); start = 1; }
else if (!radial && parts[0].StartsWith("to ", StringComparison.OrdinalIgnoreCase))
{
string direction = parts[0][3..]; bool top = direction.Contains("top"), right = direction.Contains("right"), bottom = direction.Contains("bottom"), left = direction.Contains("left");
angle = top ? right ? 45 : left ? 315 : 0 : bottom ? right ? 135 : left ? 225 : 180 : right ? 90 : left ? 270 : 180; start = 1;
}
else if (radial && (parts[0].StartsWith("circle", StringComparison.OrdinalIgnoreCase) || parts[0].StartsWith("at ", StringComparison.OrdinalIgnoreCase)))
{
string shape = parts[0]; int at = shape.IndexOf("at ", StringComparison.OrdinalIgnoreCase);
if (at >= 0)
{
string[] position = shape[(at + 3)..].Split(' ', StringSplitOptions.RemoveEmptyEntries);
double Position(string value, double fallback,double basis) => value.EndsWith('%') ? CssValue.Number(value[..^1],fallback * 100) / 100 : value switch { "left" or "top" => 0, "right" or "bottom" => 1, "center" => .5, _ => CssValue.Length(value,basis,style.Font.Size,style.Viewport,16) is { } length&&basis>0?length/basis:fallback };
if (position.Length > 0) centerX = Position(position[0],.5,width);
if (position.Length > 1) centerY = Position(position[1],.5,height);
}
start = 1;
}
double cx=centerX*width,cy=centerY*height;
double rx=Math.Max(Math.Abs(cx),Math.Abs(width-cx)),ry=Math.Max(Math.Abs(cy),Math.Abs(height-cy));
double radians=angle*Math.PI/180;
double gradientLength=radial?Math.Sqrt(rx*rx+ry*ry):Math.Abs(width*Math.Sin(radians))+Math.Abs(height*Math.Cos(radians));
var colors = new List<CssColor>(); var positions = new List<double?>();
for (int i = start; i < parts.Length; i++)
{
string[] words = CssValue.Split(parts[i], ' ').ToArray(); if (words.Length == 0) continue;
double? position = words.Length > 1 ? words[^1].EndsWith('%') ? CssValue.Number(words[^1][..^1], double.NaN) / 100 :
CssValue.Length(words[^1],gradientLength,style.Font.Size,style.Viewport,16) is { } length&&gradientLength>0?length/gradientLength:null : null;
if (position is { } number && !double.IsFinite(number)) position = null;
string color = position is null ? parts[i] : string.Join(' ', words.Take(words.Length - 1));
colors.Add(CssValue.Color(color,style.Color)); positions.Add(position);
}
if (colors.Count < 2) continue;
positions[0] ??= 0; positions[^1] ??= 1;
double previous = positions[0]!.Value;
for (int i = 1; i < positions.Count; i++) if (positions[i] is { } specified) { positions[i] = Math.Max(previous, specified); previous = positions[i]!.Value; }
int anchor = 0;
for (int i = 1; i < positions.Count; i++)
if (positions[i] is { } end)
{ double begin = positions[anchor]!.Value; for (int gap = anchor + 1; gap < i; gap++) positions[gap] = begin + (end - begin) * (gap - anchor) / (i - anchor); anchor = i; }
result.Add(new(radial,angle,centerX,centerY, colors.Select((color,index)=>new BrowserGradientStop(color,positions[index]!.Value)).ToArray()));
}
return result.ToArray();
}
public CssColor Sample(double x, double y, double width, double height)
=> Prepare(width, height).Sample(x, y);
// Box geometry is constant for every pixel of a gradient layer. Prepare it
// once per paint instead of repeating trigonometry and radius calculations
// for each device pixel. Keep the original arithmetic order for sampling.
internal BrowserGradientSampler Prepare(double width, double height) => new(this, width, height);
}
internal readonly struct BrowserGradientSampler
{
private readonly BrowserCssGradient gradient;
private readonly double halfWidth, halfHeight, centerX, centerY, dx, dy, length;
public BrowserGradientSampler(BrowserCssGradient gradient, double width, double height)
{
this.gradient = gradient;
halfWidth = width / 2; halfHeight = height / 2;
centerX = gradient.CenterX * width; centerY = gradient.CenterY * height;
if (gradient.Radial)
{
double rx = Math.Max(Math.Abs(centerX), Math.Abs(width - centerX));
double ry = Math.Max(Math.Abs(centerY), Math.Abs(height - centerY));
length = Math.Sqrt(rx * rx + ry * ry); dx = dy = 0;
}
else
{
double radians = gradient.Angle * Math.PI / 180;
dx = Math.Sin(radians); dy = -Math.Cos(radians);
length = Math.Abs(width * dx) + Math.Abs(height * dy);
}
}
public CssColor Sample(double x, double y)
{
double position;
if (gradient.Radial) position = length <= 0 ? 1 : Math.Sqrt((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY)) / length;
else position = length <= 0 ? 1 : .5 + ((x - halfWidth) * dx + (y - halfHeight) * dy) / length;
var Stops = gradient.Stops;
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 CssValue.Mix(Stops[i-1].Color,Stops[i].Color,length<=0?0:1-(position-Stops[i-1].Position)/length); }
return Stops[^1].Color;
}
}
namespace XFESystem.Browser;
public sealed record BrowserGradientStop(CssColor Color, double Position);
/// <summary>Finite, non-repeating CSS linear and circular radial gradient layers.</summary>
public sealed record BrowserCssGradient(bool Radial, double Angle, double CenterX, double CenterY, IReadOnlyList<BrowserGradientStop> Stops)
{
internal static IReadOnlyList<BrowserCssGradient> Parse(string text, CssComputedStyle style, BrowserRect? box = null)
{
if (!text.Contains("gradient(",StringComparison.OrdinalIgnoreCase)) return [];
var result = new List<BrowserCssGradient>();
foreach (string raw in CssValue.Split(text, ','))
{
string layer = raw.Trim(); bool radial = layer.StartsWith("radial-gradient(", StringComparison.OrdinalIgnoreCase);
int open = layer.IndexOf('(');
if ((!radial && !layer.StartsWith("linear-gradient(", StringComparison.OrdinalIgnoreCase)) || !layer.EndsWith(')') || open < 0 || result.Count >= 8) continue;
string[] parts = CssValue.Split(layer[(open + 1)..^1], ',').Select(part => part.Trim()).ToArray();
if (parts.Length < 2 || parts.Length > 64) continue;
double angle = 180, centerX = .5, centerY = .5; int start = 0;
double width=box?.Width??style.Viewport.Width,height=box?.Height??style.Viewport.Height;
if (!radial && parts[0].EndsWith("deg", StringComparison.OrdinalIgnoreCase)) { angle = CssValue.Number(parts[0][..^3], 180); start = 1; }
else if (!radial && parts[0].StartsWith("to ", StringComparison.OrdinalIgnoreCase))
{
string direction = parts[0][3..]; bool top = direction.Contains("top"), right = direction.Contains("right"), bottom = direction.Contains("bottom"), left = direction.Contains("left");
angle = top ? right ? 45 : left ? 315 : 0 : bottom ? right ? 135 : left ? 225 : 180 : right ? 90 : left ? 270 : 180; start = 1;
}
else if (radial && (parts[0].StartsWith("circle", StringComparison.OrdinalIgnoreCase) || parts[0].StartsWith("at ", StringComparison.OrdinalIgnoreCase)))
{
string shape = parts[0]; int at = shape.IndexOf("at ", StringComparison.OrdinalIgnoreCase);
if (at >= 0)
{
string[] position = shape[(at + 3)..].Split(' ', StringSplitOptions.RemoveEmptyEntries);
double Position(string value, double fallback,double basis) => value.EndsWith('%') ? CssValue.Number(value[..^1],fallback * 100) / 100 : value switch { "left" or "top" => 0, "right" or "bottom" => 1, "center" => .5, _ => CssValue.Length(value,basis,style.Font.Size,style.Viewport,16) is { } length&&basis>0?length/basis:fallback };
if (position.Length > 0) centerX = Position(position[0],.5,width);
if (position.Length > 1) centerY = Position(position[1],.5,height);
}
start = 1;
}
double cx=centerX*width,cy=centerY*height;
double rx=Math.Max(Math.Abs(cx),Math.Abs(width-cx)),ry=Math.Max(Math.Abs(cy),Math.Abs(height-cy));
double radians=angle*Math.PI/180;
double gradientLength=radial?Math.Sqrt(rx*rx+ry*ry):Math.Abs(width*Math.Sin(radians))+Math.Abs(height*Math.Cos(radians));
var colors = new List<CssColor>(); var positions = new List<double?>();
for (int i = start; i < parts.Length; i++)
{
string[] words = CssValue.Split(parts[i], ' ').ToArray(); if (words.Length == 0) continue;
double? position = words.Length > 1 ? words[^1].EndsWith('%') ? CssValue.Number(words[^1][..^1], double.NaN) / 100 :
CssValue.Length(words[^1],gradientLength,style.Font.Size,style.Viewport,16) is { } length&&gradientLength>0?length/gradientLength:null : null;
if (position is { } number && !double.IsFinite(number)) position = null;
string color = position is null ? parts[i] : string.Join(' ', words.Take(words.Length - 1));
colors.Add(CssValue.Color(color,style.Color)); positions.Add(position);
}
if (colors.Count < 2) continue;
positions[0] ??= 0; positions[^1] ??= 1;
double previous = positions[0]!.Value;
for (int i = 1; i < positions.Count; i++) if (positions[i] is { } specified) { positions[i] = Math.Max(previous, specified); previous = positions[i]!.Value; }
int anchor = 0;
for (int i = 1; i < positions.Count; i++)
if (positions[i] is { } end)
{ double begin = positions[anchor]!.Value; for (int gap = anchor + 1; gap < i; gap++) positions[gap] = begin + (end - begin) * (gap - anchor) / (i - anchor); anchor = i; }
result.Add(new(radial,angle,centerX,centerY, colors.Select((color,index)=>new BrowserGradientStop(color,positions[index]!.Value)).ToArray()));
}
return result.ToArray();
}
public CssColor Sample(double x, double y, double width, double height)
=> Prepare(width, height).Sample(x, y);
// Box geometry is constant for every pixel of a gradient layer. Prepare it
// once per paint instead of repeating trigonometry and radius calculations
// for each device pixel. Keep the original arithmetic order for sampling.
internal BrowserGradientSampler Prepare(double width, double height) => new(this, width, height);
}
internal readonly struct BrowserGradientSampler
{
private readonly BrowserCssGradient gradient;
private readonly double halfWidth, halfHeight, centerX, centerY, dx, dy, length;
public BrowserGradientSampler(BrowserCssGradient gradient, double width, double height)
{
this.gradient = gradient;
halfWidth = width / 2; halfHeight = height / 2;
centerX = gradient.CenterX * width; centerY = gradient.CenterY * height;
if (gradient.Radial)
{
double rx = Math.Max(Math.Abs(centerX), Math.Abs(width - centerX));
double ry = Math.Max(Math.Abs(centerY), Math.Abs(height - centerY));
length = Math.Sqrt(rx * rx + ry * ry); dx = dy = 0;
}
else
{
double radians = gradient.Angle * Math.PI / 180;
dx = Math.Sin(radians); dy = -Math.Cos(radians);
length = Math.Abs(width * dx) + Math.Abs(height * dy);
}
}
public CssColor Sample(double x, double y)
{
double position;
if (gradient.Radial) position = length <= 0 ? 1 : Math.Sqrt((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY)) / length;
else position = length <= 0 ? 1 : .5 + ((x - halfWidth) * dx + (y - halfHeight) * dy) / length;
var Stops = gradient.Stops;
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 CssValue.Mix(Stops[i-1].Color,Stops[i].Color,length<=0?0:1-(position-Stops[i-1].Position)/length); }
return Stops[^1].Color;
}
}