namespace XFESystem.Browser; public sealed record BrowserGradientStop(CssColor Color, double Position); /// Finite, non-repeating CSS linear and circular radial gradient layers. public sealed record BrowserCssGradient(bool Radial, double Angle, double CenterX, double CenterY, IReadOnlyList Stops) { internal static IReadOnlyList Parse(string text, CssComputedStyle style, BrowserRect? box = null) { if (!text.Contains("gradient(",StringComparison.OrdinalIgnoreCase)) return []; var result = new List(); 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(); var positions = new List(); 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