// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Numerics; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Drawing { /// /// Represents a line segment that contains radii and angles that will be rendered as a elliptical arc. /// public class ArcLineSegment : ILineSegment { private const float ZeroTolerance = 1e-05F; private readonly PointF[] linePoints; /// /// Initializes a new instance of the class. /// /// The absolute coordinates of the current point on the path. /// The absolute coordinates of the final point of the arc. /// The radii of the ellipse (also known as its semi-major and semi-minor axes). /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. /// /// The large arc flag, and is if an arc spanning less than or equal to 180 degrees /// is chosen, or if an arc spanning greater than 180 degrees is chosen. /// /// /// The sweep flag, and is if the line joining center to arc sweeps through decreasing /// angles, or if it sweeps through increasing angles. /// public ArcLineSegment(PointF from, PointF to, SizeF radius, float rotation, bool largeArc, bool sweep) { rotation = GeometryUtilities.DegreeToRadian(rotation); bool ellipse = largeArc && ((Vector2)to - (Vector2)from).LengthSquared() < ZeroTolerance && radius.Width > 0 && radius.Height > 0; if (ellipse) { // The circle always has a start angle of 0 which is positioned at 3 o'clock. // This means the centre point is to the left of the start position. Vector2 center = (Vector2)from - new Vector2(radius.Width, 0); this.linePoints = EllipticArcToBezierCurve(from, center, radius, rotation, 0, sweep ? 2 * MathF.PI : -2 * MathF.PI); } else { this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep); } this.Bounds = CalculateBounds(this.linePoints); } /// /// Initializes a new instance of the class. /// /// The coordinates of the center of the ellipse. /// The radii of the ellipse (also known as its semi-major and semi-minor axes). /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. /// /// The start angle of the elliptical arc prior to the stretch and rotate operations. /// (0 is at the 3 o'clock position of the arc's circle). /// /// The angle between and the end of the arc. public ArcLineSegment(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) { rotation = GeometryUtilities.DegreeToRadian(rotation); startAngle = GeometryUtilities.DegreeToRadian(Clamp(startAngle, -360F, 360F)); sweepAngle = GeometryUtilities.DegreeToRadian(Clamp(sweepAngle, -360F, 360F)); Vector2 from = EllipticArcPoint(center, radius, rotation, startAngle); Vector2 to = EllipticArcPoint(center, radius, rotation, startAngle + sweepAngle); bool largeArc = Math.Abs(sweepAngle) > MathF.PI; bool sweep = sweepAngle > 0; bool ellipse = largeArc && (to - from).LengthSquared() < ZeroTolerance && radius.Width > 0 && radius.Height > 0; if (ellipse) { this.linePoints = EllipticArcToBezierCurve(from, center, radius, rotation, startAngle, sweepAngle); } else { this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep); } this.Bounds = CalculateBounds(this.linePoints); } private ArcLineSegment(PointF[] linePoints) { this.linePoints = linePoints; this.Bounds = CalculateBounds(linePoints); } /// public PointF StartPoint => this.linePoints[0]; /// public PointF EndPoint => this.linePoints[^1]; /// public RectangleF Bounds { get; } /// public int LinearVertexCount(Vector2 scale) => this.linePoints.Length; /// public void CopyTo(Span destination, bool skipFirstPoint, Vector2 scale) { int startIndex = skipFirstPoint ? 1 : 0; ReadOnlySpan source = this.linePoints.AsSpan(startIndex); if (scale == Vector2.One) { source.CopyTo(destination); return; } for (int i = 0; i < source.Length; i++) { destination[i] = new PointF(source[i].X * scale.X, source[i].Y * scale.Y); } } /// /// Transforms the current using specified matrix. /// /// The transformation matrix. /// An with the matrix applied to it. public ILineSegment Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { return this; } PointF[] transformedPoints = new PointF[this.linePoints.Length]; for (int i = 0; i < this.linePoints.Length; i++) { transformedPoints[i] = PointF.Transform(this.linePoints[i], matrix); } return new ArcLineSegment(transformedPoints); } /// ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix); /// /// Computes the bounds for the retained linearized arc points. /// private static RectangleF CalculateBounds(ReadOnlySpan points) { float minX = float.MaxValue; float minY = float.MaxValue; float maxX = float.MinValue; float maxY = float.MinValue; for (int i = 0; i < points.Length; i++) { PointF point = points[i]; minX = MathF.Min(minX, point.X); minY = MathF.Min(minY, point.Y); maxX = MathF.Max(maxX, point.X); maxY = MathF.Max(maxY, point.Y); } return RectangleF.FromLTRB(minX, minY, maxX, maxY); } private static PointF[] EllipticArcFromEndParams( PointF from, PointF to, SizeF radius, float rotation, bool largeArc, bool sweep) { Vector2 absRadius = Vector2.Abs(radius); if (EllipticArcOutOfRange(from, to, radius)) { return [from, to]; } EndpointToCenterArcParams(from, to, ref absRadius, rotation, largeArc, sweep, out Vector2 center, out Vector2 angles); return EllipticArcToBezierCurve(from, center, absRadius, rotation, angles.X, angles.Y); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool EllipticArcOutOfRange(Vector2 from, Vector2 to, Vector2 radius) { // F.6.2 Out-of-range parameters radius = Vector2.Abs(radius); float len = (to - from).LengthSquared(); if (len < ZeroTolerance) { return true; } if (radius.X < ZeroTolerance || radius.Y < ZeroTolerance) { return true; } return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector2 EllipticArcDerivative(Vector2 r, float xAngle, float t) => new( (-r.X * MathF.Cos(xAngle) * MathF.Sin(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Cos(t)), (-r.X * MathF.Sin(xAngle) * MathF.Sin(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Cos(t))); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector2 EllipticArcPoint(Vector2 c, Vector2 r, float xAngle, float t) => new( c.X + (r.X * MathF.Cos(xAngle) * MathF.Cos(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Sin(t)), c.Y + (r.X * MathF.Sin(xAngle) * MathF.Cos(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Sin(t))); private static PointF[] EllipticArcToBezierCurve(Vector2 from, Vector2 center, Vector2 radius, float xAngle, float startAngle, float sweepAngle) { float s = startAngle; float e = s + sweepAngle; bool neg = e < s; float sign = neg ? -1 : 1; float remain = Math.Abs(e - s); int curveCount = Math.Max((int)MathF.Ceiling(remain / (MathF.PI / 4F)), 1); // Arc flattening retains the final point array, so use the builder to avoid the // intermediate collection and copy a list would generate. FlattenedPointBuilder points = new(curveCount * 4); Vector2 prev = EllipticArcPoint(center, radius, xAngle, s); while (remain > ZeroTolerance) { float step = (float)Math.Min(remain, Math.PI / 4); float signStep = step * sign; Vector2 p1 = prev; Vector2 p2 = EllipticArcPoint(center, radius, xAngle, s + signStep); float alphaT = (float)Math.Tan(signStep / 2); float alpha = (float)(Math.Sin(signStep) * (Math.Sqrt(4 + (3 * alphaT * alphaT)) - 1) / 3); Vector2 q1 = p1 + (alpha * EllipticArcDerivative(radius, xAngle, s)); Vector2 q2 = p2 - (alpha * EllipticArcDerivative(radius, xAngle, s + signStep)); CubicBezierLineSegment bezier = new(from, q1, q2, p2); int bezierCount = bezier.LinearVertexCount(Vector2.One); Span destination = points.GetAppendSpan(bezierCount); bezier.CopyTo(destination, skipFirstPoint: false, Vector2.One); points.Advance(bezierCount); from = p2; s += signStep; remain -= step; prev = p2; } return points.Detach(); } private static void EndpointToCenterArcParams( Vector2 p1, Vector2 p2, ref Vector2 r, float xRotation, bool flagA, bool flagS, out Vector2 center, out Vector2 angles) { double rX = Math.Abs(r.X); double rY = Math.Abs(r.Y); // (F.6.5.1) double dx2 = (p1.X - p2.X) / 2.0; double dy2 = (p1.Y - p2.Y) / 2.0; double x1p = (Math.Cos(xRotation) * dx2) + (Math.Sin(xRotation) * dy2); double y1p = (-Math.Sin(xRotation) * dx2) + (Math.Cos(xRotation) * dy2); // (F.6.5.2) double rxs = rX * rX; double rys = rY * rY; double x1ps = x1p * x1p; double y1ps = y1p * y1p; // check if the radius is too small `pq < 0`, when `dq > rxs * rys` (see below) // cr is the ratio (dq : rxs * rys) double cr = (x1ps / rxs) + (y1ps / rys); if (cr > 1) { // scale up rX,rY equally so cr == 1 double s = Math.Sqrt(cr); rX = s * rX; rY = s * rY; rxs = rX * rX; rys = rY * rY; } double dq = (rxs * y1ps) + (rys * x1ps); double pq = ((rxs * rys) - dq) / dq; double q = Math.Sqrt(Math.Max(0, pq)); // Use Max to account for float precision if (flagA == flagS) { q = -q; } double cxp = q * rX * y1p / rY; double cyp = -q * rY * x1p / rX; // (F.6.5.3) double cx = (Math.Cos(xRotation) * cxp) - (Math.Sin(xRotation) * cyp) + ((p1.X + p2.X) / 2); double cy = (Math.Sin(xRotation) * cxp) + (Math.Cos(xRotation) * cyp) + ((p1.Y + p2.Y) / 2); // (F.6.5.5) double theta = SvgAngle(1, 0, (x1p - cxp) / rX, (y1p - cyp) / rY); // (F.6.5.6) double delta = SvgAngle((x1p - cxp) / rX, (y1p - cyp) / rY, (-x1p - cxp) / rX, (-y1p - cyp) / rY); delta %= Math.PI * 2; if (!flagS && delta > 0) { delta -= 2 * Math.PI; } if (flagS && delta < 0) { delta += 2 * Math.PI; } r = new Vector2((float)rX, (float)rY); center = new Vector2((float)cx, (float)cy); angles = new Vector2((float)theta, (float)delta); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float Clamp(float val, float min, float max) { if (val < min) { return min; } else if (val > max) { return max; } else { return val; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float SvgAngle(double ux, double uy, double vx, double vy) { Vector2 u = new((float)ux, (float)uy); Vector2 v = new((float)vx, (float)vy); // (F.6.5.4) float dot = Vector2.Dot(u, v); float len = u.Length() * v.Length(); float ang = (float)Math.Acos(Clamp(dot / len, -1, 1)); // floating point precision, slightly over values appear if (((u.X * v.Y) - (u.Y * v.X)) < 0) { ang = -ang; } return ang; } } }