namespace XFESystem.Browser;
/// <summary>CSS 2D column-vector matrix. Multiplication preserves CSS function order.</summary>
public readonly record struct BrowserAffineTransform(double A, double B, double C, double D, double E, double F)
{
public static BrowserAffineTransform Identity => new(1, 0, 0, 1, 0, 0);
public bool IsIdentity => this == Identity;
public bool IsTranslation => A == 1 && B == 0 && C == 0 && D == 1;
public (double X, double Y) Point(double x, double y) => (A * x + C * y + E, B * x + D * y + F);
public BrowserRect Bounds(BrowserRect r)
{
var p = Point(r.Left, r.Top); var q = Point(r.Right, r.Top);
var s = Point(r.Left, r.Bottom); var t = Point(r.Right, r.Bottom);
double left = Math.Min(Math.Min(p.X, q.X), Math.Min(s.X, t.X)), top = Math.Min(Math.Min(p.Y, q.Y), Math.Min(s.Y, t.Y));
return new(left, top, Math.Max(Math.Max(p.X, q.X), Math.Max(s.X, t.X)) - left, Math.Max(Math.Max(p.Y, q.Y), Math.Max(s.Y, t.Y)) - top);
}
public static BrowserAffineTransform operator *(BrowserAffineTransform x, BrowserAffineTransform y) => new(
x.A * y.A + x.C * y.B, x.B * y.A + x.D * y.B, x.A * y.C + x.C * y.D, x.B * y.C + x.D * y.D,
x.A * y.E + x.C * y.F + x.E, x.B * y.E + x.D * y.F + x.F);
public static BrowserAffineTransform Translation(double x, double y) => new(1, 0, 0, 1, x, y);
public bool TryInverse(out BrowserAffineTransform inverse)
{
double determinant = A * D - B * C;
if (determinant == 0 || !double.IsFinite(determinant)) { inverse = default; return false; }
inverse = new(D / determinant, -B / determinant, -C / determinant, A / determinant,
(C * F - D * E) / determinant, (B * E - A * F) / determinant);
return inverse.Finite;
}
internal bool Finite => double.IsFinite(A) && double.IsFinite(B) && double.IsFinite(C) && double.IsFinite(D) && double.IsFinite(E) && double.IsFinite(F);
internal static bool TryParse(string text, BrowserRect reference, CssComputedStyle style, out BrowserAffineTransform matrix)
{
matrix = Identity; text = text.Trim().ToLowerInvariant();
if (text == "none") return true;
int count = 0;
for (int offset = 0; offset < text.Length;)
{
if (++count > 64) return false;
int open = text.IndexOf('(', offset); if (open < 0) return false;
string name = text[offset..open].Trim(); int depth = 1, close = open + 1;
while (close < text.Length && depth > 0) { if (text[close] == '(') depth++; else if (text[close] == ')') depth--; close++; }
if (depth != 0) return false;
var parts = new List<string>(); string args = text[(open + 1)..(close - 1)]; int start = 0; depth = 0;
for (int index = 0; index <= args.Length; index++)
{
if (index == args.Length || depth == 0 && (args[index] == ',' || char.IsWhiteSpace(args[index])))
{ if (index > start) parts.Add(args[start..index].Trim()); start = index + 1; }
else if (args[index] == '(') depth++; else if (args[index] == ')') depth--;
}
double Number(int index) => index < parts.Count ? CssValue.Number(parts[index], double.NaN) : double.NaN;
double Length(int index, double basis) => index < parts.Count ? CssValue.Length(parts[index], basis, style.Font.Size, style.Viewport, 16) ?? double.NaN : double.NaN;
double Angle(int index)
{
if (index >= parts.Count) return double.NaN;
string part = parts[index];
foreach (var unit in new[] { ("deg", Math.PI / 180), ("grad", Math.PI / 200), ("rad", 1d), ("turn", Math.PI * 2) })
if (part.EndsWith(unit.Item1, StringComparison.Ordinal)) return CssValue.Number(part[..^unit.Item1.Length], double.NaN) * unit.Item2;
return part == "0" ? 0 : double.NaN;
}
BrowserAffineTransform next;
switch (name)
{
case "translate" when parts.Count is 1 or 2: next = Translation(Length(0, reference.Width), parts.Count == 1 ? 0 : Length(1, reference.Height)); break;
case "translatex" when parts.Count == 1: next = Translation(Length(0, reference.Width), 0); break;
case "translatey" when parts.Count == 1: next = Translation(0, Length(0, reference.Height)); break;
case "translate3d" when parts.Count == 3 && Length(2, 0) == 0: next = Translation(Length(0, reference.Width), Length(1, reference.Height)); break;
case "scale" when parts.Count is 1 or 2: next = new(Number(0), 0, 0, parts.Count == 1 ? Number(0) : Number(1), 0, 0); break;
case "scalex" when parts.Count == 1: next = new(Number(0), 0, 0, 1, 0, 0); break;
case "scaley" when parts.Count == 1: next = new(1, 0, 0, Number(0), 0, 0); break;
case "rotate" when parts.Count == 1:
double angle = Angle(0); next = new(Math.Cos(angle), Math.Sin(angle), -Math.Sin(angle), Math.Cos(angle), 0, 0); break;
case "skew" when parts.Count is 1 or 2: next = new(1, parts.Count == 1 ? 0 : Math.Tan(Angle(1)), Math.Tan(Angle(0)), 1, 0, 0); break;
case "skewx" when parts.Count == 1: next = new(1, 0, Math.Tan(Angle(0)), 1, 0, 0); break;
case "skewy" when parts.Count == 1: next = new(1, Math.Tan(Angle(0)), 0, 1, 0, 0); break;
case "matrix" when parts.Count == 6: next = new(Number(0), Number(1), Number(2), Number(3), Number(4), Number(5)); break;
default: return false;
}
matrix *= next;
if (!matrix.Finite || Math.Abs(matrix.A) > 1000000 || Math.Abs(matrix.B) > 1000000 || Math.Abs(matrix.C) > 1000000 || Math.Abs(matrix.D) > 1000000 || Math.Abs(matrix.E) > 1000000 || Math.Abs(matrix.F) > 1000000) return false;
offset = close; while (offset < text.Length && char.IsWhiteSpace(text[offset])) offset++;
}
return count > 0;
}
internal static (double X, double Y) Origin(CssComputedStyle style, BrowserRect box)
{
var parts = style.Get("transform-origin", "50% 50%").Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length is < 1 or > 3) return (box.X + box.Width / 2, box.Y + box.Height / 2);
string x = parts[0], y = parts.Length > 1 ? parts[1] : "center";
if (x is "top" or "bottom" || y is "left" or "right") (x, y) = (y, x);
double Resolve(string value, double basis) => value switch { "center" => basis / 2, "left" or "top" => 0, "right" or "bottom" => basis,
_ => CssValue.Length(value, basis, style.Font.Size, style.Viewport, 16) ?? basis / 2 };
return (box.X + Resolve(x, box.Width), box.Y + Resolve(y, box.Height));
}
}
namespace XFESystem.Browser;
/// <summary>CSS 2D column-vector matrix. Multiplication preserves CSS function order.</summary>
public readonly record struct BrowserAffineTransform(double A, double B, double C, double D, double E, double F)
{
public static BrowserAffineTransform Identity => new(1, 0, 0, 1, 0, 0);
public bool IsIdentity => this == Identity;
public bool IsTranslation => A == 1 && B == 0 && C == 0 && D == 1;
public (double X, double Y) Point(double x, double y) => (A * x + C * y + E, B * x + D * y + F);
public BrowserRect Bounds(BrowserRect r)
{
var p = Point(r.Left, r.Top); var q = Point(r.Right, r.Top);
var s = Point(r.Left, r.Bottom); var t = Point(r.Right, r.Bottom);
double left = Math.Min(Math.Min(p.X, q.X), Math.Min(s.X, t.X)), top = Math.Min(Math.Min(p.Y, q.Y), Math.Min(s.Y, t.Y));
return new(left, top, Math.Max(Math.Max(p.X, q.X), Math.Max(s.X, t.X)) - left, Math.Max(Math.Max(p.Y, q.Y), Math.Max(s.Y, t.Y)) - top);
}
public static BrowserAffineTransform operator *(BrowserAffineTransform x, BrowserAffineTransform y) => new(
x.A * y.A + x.C * y.B, x.B * y.A + x.D * y.B, x.A * y.C + x.C * y.D, x.B * y.C + x.D * y.D,
x.A * y.E + x.C * y.F + x.E, x.B * y.E + x.D * y.F + x.F);
public static BrowserAffineTransform Translation(double x, double y) => new(1, 0, 0, 1, x, y);
public bool TryInverse(out BrowserAffineTransform inverse)
{
double determinant = A * D - B * C;
if (determinant == 0 || !double.IsFinite(determinant)) { inverse = default; return false; }
inverse = new(D / determinant, -B / determinant, -C / determinant, A / determinant,
(C * F - D * E) / determinant, (B * E - A * F) / determinant);
return inverse.Finite;
}
internal bool Finite => double.IsFinite(A) && double.IsFinite(B) && double.IsFinite(C) && double.IsFinite(D) && double.IsFinite(E) && double.IsFinite(F);
internal static bool TryParse(string text, BrowserRect reference, CssComputedStyle style, out BrowserAffineTransform matrix)
{
matrix = Identity; text = text.Trim().ToLowerInvariant();
if (text == "none") return true;
int count = 0;
for (int offset = 0; offset < text.Length;)
{
if (++count > 64) return false;
int open = text.IndexOf('(', offset); if (open < 0) return false;
string name = text[offset..open].Trim(); int depth = 1, close = open + 1;
while (close < text.Length && depth > 0) { if (text[close] == '(') depth++; else if (text[close] == ')') depth--; close++; }
if (depth != 0) return false;
var parts = new List<string>(); string args = text[(open + 1)..(close - 1)]; int start = 0; depth = 0;
for (int index = 0; index <= args.Length; index++)
{
if (index == args.Length || depth == 0 && (args[index] == ',' || char.IsWhiteSpace(args[index])))
{ if (index > start) parts.Add(args[start..index].Trim()); start = index + 1; }
else if (args[index] == '(') depth++; else if (args[index] == ')') depth--;
}
double Number(int index) => index < parts.Count ? CssValue.Number(parts[index], double.NaN) : double.NaN;
double Length(int index, double basis) => index < parts.Count ? CssValue.Length(parts[index], basis, style.Font.Size, style.Viewport, 16) ?? double.NaN : double.NaN;
double Angle(int index)
{
if (index >= parts.Count) return double.NaN;
string part = parts[index];
foreach (var unit in new[] { ("deg", Math.PI / 180), ("grad", Math.PI / 200), ("rad", 1d), ("turn", Math.PI * 2) })
if (part.EndsWith(unit.Item1, StringComparison.Ordinal)) return CssValue.Number(part[..^unit.Item1.Length], double.NaN) * unit.Item2;
return part == "0" ? 0 : double.NaN;
}
BrowserAffineTransform next;
switch (name)
{
case "translate" when parts.Count is 1 or 2: next = Translation(Length(0, reference.Width), parts.Count == 1 ? 0 : Length(1, reference.Height)); break;
case "translatex" when parts.Count == 1: next = Translation(Length(0, reference.Width), 0); break;
case "translatey" when parts.Count == 1: next = Translation(0, Length(0, reference.Height)); break;
case "translate3d" when parts.Count == 3 && Length(2, 0) == 0: next = Translation(Length(0, reference.Width), Length(1, reference.Height)); break;
case "scale" when parts.Count is 1 or 2: next = new(Number(0), 0, 0, parts.Count == 1 ? Number(0) : Number(1), 0, 0); break;
case "scalex" when parts.Count == 1: next = new(Number(0), 0, 0, 1, 0, 0); break;
case "scaley" when parts.Count == 1: next = new(1, 0, 0, Number(0), 0, 0); break;
case "rotate" when parts.Count == 1:
double angle = Angle(0); next = new(Math.Cos(angle), Math.Sin(angle), -Math.Sin(angle), Math.Cos(angle), 0, 0); break;
case "skew" when parts.Count is 1 or 2: next = new(1, parts.Count == 1 ? 0 : Math.Tan(Angle(1)), Math.Tan(Angle(0)), 1, 0, 0); break;
case "skewx" when parts.Count == 1: next = new(1, 0, Math.Tan(Angle(0)), 1, 0, 0); break;
case "skewy" when parts.Count == 1: next = new(1, Math.Tan(Angle(0)), 0, 1, 0, 0); break;
case "matrix" when parts.Count == 6: next = new(Number(0), Number(1), Number(2), Number(3), Number(4), Number(5)); break;
default: return false;
}
matrix *= next;
if (!matrix.Finite || Math.Abs(matrix.A) > 1000000 || Math.Abs(matrix.B) > 1000000 || Math.Abs(matrix.C) > 1000000 || Math.Abs(matrix.D) > 1000000 || Math.Abs(matrix.E) > 1000000 || Math.Abs(matrix.F) > 1000000) return false;
offset = close; while (offset < text.Length && char.IsWhiteSpace(text[offset])) offset++;
}
return count > 0;
}
internal static (double X, double Y) Origin(CssComputedStyle style, BrowserRect box)
{
var parts = style.Get("transform-origin", "50% 50%").Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length is < 1 or > 3) return (box.X + box.Width / 2, box.Y + box.Height / 2);
string x = parts[0], y = parts.Length > 1 ? parts[1] : "center";
if (x is "top" or "bottom" || y is "left" or "right") (x, y) = (y, x);
double Resolve(string value, double basis) => value switch { "center" => basis / 2, "left" or "top" => 0, "right" or "bottom" => basis,
_ => CssValue.Length(value, basis, style.Font.Size, style.Viewport, 16) ?? basis / 2 };
return (box.X + Resolve(x, box.Width), box.Y + Resolve(y, box.Height));
}
}