|
| 1 | +// Copyright (c) Six Labors. |
| 2 | +// Licensed under the Apache License, Version 2.0. |
| 3 | + |
| 4 | +using System; |
| 5 | +using System.Collections.Generic; |
| 6 | +using System.Numerics; |
| 7 | +using System.Runtime.CompilerServices; |
| 8 | + |
| 9 | +namespace SixLabors.ImageSharp.Drawing |
| 10 | +{ |
| 11 | + /// <summary> |
| 12 | + /// Represents a line segment that contains radii and angles that will be rendered as a elliptical arc. |
| 13 | + /// </summary> |
| 14 | + public class ArcLineSegment : ILineSegment |
| 15 | + { |
| 16 | + private const float ZeroTolerance = 1e-05F; |
| 17 | + private readonly PointF[] linePoints; |
| 18 | + |
| 19 | + /// <summary> |
| 20 | + /// Initializes a new instance of the <see cref="ArcLineSegment"/> class. |
| 21 | + /// </summary> |
| 22 | + /// <param name="from">The absolute coordinates of the current point on the path.</param> |
| 23 | + /// <param name="to">The absolute coordinates of the final point of the arc.</param> |
| 24 | + /// <param name="radius">The radii of the ellipse (also known as its semi-major and semi-minor axes).</param> |
| 25 | + /// <param name="rotation">The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse.</param> |
| 26 | + /// <param name="largeArc"> |
| 27 | + /// The large arc flag, and is <see langword="false"/> if an arc spanning less than or equal to 180 degrees |
| 28 | + /// is chosen, or <see langword="true"/> if an arc spanning greater than 180 degrees is chosen. |
| 29 | + /// </param> |
| 30 | + /// <param name="sweep"> |
| 31 | + /// The sweep flag, and is <see langword="false"/> if the line joining center to arc sweeps through decreasing |
| 32 | + /// angles, or <see langword="true"/> if it sweeps through increasing angles. |
| 33 | + /// </param> |
| 34 | + public ArcLineSegment(PointF from, PointF to, SizeF radius, float rotation, bool largeArc, bool sweep) |
| 35 | + { |
| 36 | + rotation = GeometryUtilities.DegreeToRadian(rotation); |
| 37 | + bool circle = largeArc && ((Vector2)to - (Vector2)from).LengthSquared() < ZeroTolerance && radius.Width > 0 && radius.Height > 0; |
| 38 | + this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep, circle); |
| 39 | + this.EndPoint = this.linePoints[this.linePoints.Length - 1]; |
| 40 | + } |
| 41 | + |
| 42 | + /// <summary> |
| 43 | + /// Initializes a new instance of the <see cref="ArcLineSegment"/> class. |
| 44 | + /// </summary> |
| 45 | + /// <param name="center">The coordinates of the center of the ellipse.</param> |
| 46 | + /// <param name="radius">The radii of the ellipse (also known as its semi-major and semi-minor axes).</param> |
| 47 | + /// <param name="rotation">The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse.</param> |
| 48 | + /// <param name="startAngle"> |
| 49 | + /// The start angle of the elliptical arc prior to the stretch and rotate operations. |
| 50 | + /// (0 is at the 3 o'clock position of the arc's circle). |
| 51 | + /// </param> |
| 52 | + /// <param name="sweepAngle">The angle between <paramref name="startAngle"/> and the end of the arc.</param> |
| 53 | + public ArcLineSegment(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) |
| 54 | + { |
| 55 | + rotation = GeometryUtilities.DegreeToRadian(rotation); |
| 56 | + startAngle = GeometryUtilities.DegreeToRadian(Clamp(startAngle, -360F, 360F)); |
| 57 | + sweepAngle = GeometryUtilities.DegreeToRadian(Clamp(sweepAngle, -360F, 360F)); |
| 58 | + |
| 59 | + Vector2 from = EllipticArcPoint(center, radius, rotation, startAngle); |
| 60 | + Vector2 to = EllipticArcPoint(center, radius, rotation, startAngle + sweepAngle); |
| 61 | + |
| 62 | + bool largeArc = Math.Abs(sweepAngle) > MathF.PI; |
| 63 | + bool sweep = sweepAngle > 0; |
| 64 | + bool circle = largeArc && (to - from).LengthSquared() < ZeroTolerance && radius.Width > 0 && radius.Height > 0; |
| 65 | + |
| 66 | + this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep, circle); |
| 67 | + this.EndPoint = this.linePoints[this.linePoints.Length - 1]; |
| 68 | + } |
| 69 | + |
| 70 | + private ArcLineSegment(PointF[] linePoints) |
| 71 | + { |
| 72 | + this.linePoints = linePoints; |
| 73 | + this.EndPoint = this.linePoints[this.linePoints.Length - 1]; |
| 74 | + } |
| 75 | + |
| 76 | + /// <inheritdoc/> |
| 77 | + public PointF EndPoint { get; } |
| 78 | + |
| 79 | + /// <inheritdoc/> |
| 80 | + public ReadOnlyMemory<PointF> Flatten() => this.linePoints; |
| 81 | + |
| 82 | + /// <summary> |
| 83 | + /// Transforms the current <see cref="ArcLineSegment"/> using specified matrix. |
| 84 | + /// </summary> |
| 85 | + /// <param name="matrix">The transformation matrix.</param> |
| 86 | + /// <returns>An <see cref="ArcLineSegment"/> with the matrix applied to it.</returns> |
| 87 | + public ILineSegment Transform(Matrix3x2 matrix) |
| 88 | + { |
| 89 | + if (matrix.IsIdentity) |
| 90 | + { |
| 91 | + return this; |
| 92 | + } |
| 93 | + |
| 94 | + var transformedPoints = new PointF[this.linePoints.Length]; |
| 95 | + for (int i = 0; i < this.linePoints.Length; i++) |
| 96 | + { |
| 97 | + transformedPoints[i] = PointF.Transform(this.linePoints[i], matrix); |
| 98 | + } |
| 99 | + |
| 100 | + return new ArcLineSegment(transformedPoints); |
| 101 | + } |
| 102 | + |
| 103 | + /// <inheritdoc/> |
| 104 | + ILineSegment ILineSegment.Transform(Matrix3x2 matrix) => this.Transform(matrix); |
| 105 | + |
| 106 | + private static PointF[] EllipticArcFromEndParams(PointF from, PointF to, SizeF radius, float rotation, bool largeArc, bool sweep, bool circle) |
| 107 | + { |
| 108 | + { |
| 109 | + var absRadius = Vector2.Abs(radius); |
| 110 | + |
| 111 | + if (circle) |
| 112 | + { |
| 113 | + // It's a circle. SVG arcs cannot handle this so let's hack together our own angles. |
| 114 | + // This appears to match the behavior of Web CanvasRenderingContext2D.arc(). |
| 115 | + // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arc |
| 116 | + Vector2 center = (Vector2)from - new Vector2(absRadius.X, 0); |
| 117 | + return EllipticArcToBezierCurve(from, center, absRadius, rotation, 0, 2 * MathF.PI); |
| 118 | + } |
| 119 | + else |
| 120 | + { |
| 121 | + if (EllipticArcOutOfRange(from, to, radius)) |
| 122 | + { |
| 123 | + return new[] { from, to }; |
| 124 | + } |
| 125 | + |
| 126 | + float xRotation = rotation; |
| 127 | + EndpointToCenterArcParams(from, to, ref absRadius, xRotation, largeArc, sweep, out Vector2 center, out Vector2 angles); |
| 128 | + |
| 129 | + return EllipticArcToBezierCurve(from, center, absRadius, xRotation, angles.X, angles.Y); |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| 135 | + private static bool EllipticArcOutOfRange(Vector2 from, Vector2 to, Vector2 radius) |
| 136 | + { |
| 137 | + // F.6.2 Out-of-range parameters |
| 138 | + radius = Vector2.Abs(radius); |
| 139 | + float len = (to - from).LengthSquared(); |
| 140 | + if (len < ZeroTolerance) |
| 141 | + { |
| 142 | + return true; |
| 143 | + } |
| 144 | + |
| 145 | + if (radius.X < ZeroTolerance || radius.Y < ZeroTolerance) |
| 146 | + { |
| 147 | + return true; |
| 148 | + } |
| 149 | + |
| 150 | + return false; |
| 151 | + } |
| 152 | + |
| 153 | + [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| 154 | + private static Vector2 EllipticArcDerivative(Vector2 r, float xAngle, float t) |
| 155 | + => new( |
| 156 | + (-r.X * MathF.Cos(xAngle) * MathF.Sin(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Cos(t)), |
| 157 | + (-r.X * MathF.Sin(xAngle) * MathF.Sin(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Cos(t))); |
| 158 | + |
| 159 | + [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| 160 | + private static Vector2 EllipticArcPoint(Vector2 c, Vector2 r, float xAngle, float t) |
| 161 | + => new( |
| 162 | + c.X + (r.X * MathF.Cos(xAngle) * MathF.Cos(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Sin(t)), |
| 163 | + c.Y + (r.X * MathF.Sin(xAngle) * MathF.Cos(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Sin(t))); |
| 164 | + |
| 165 | + private static PointF[] EllipticArcToBezierCurve(Vector2 from, Vector2 center, Vector2 radius, float xAngle, float startAngle, float sweepAngle) |
| 166 | + { |
| 167 | + List<PointF> points = new(); |
| 168 | + |
| 169 | + float s = startAngle; |
| 170 | + float e = s + sweepAngle; |
| 171 | + bool neg = e < s; |
| 172 | + float sign = neg ? -1 : 1; |
| 173 | + float remain = Math.Abs(e - s); |
| 174 | + |
| 175 | + Vector2 prev = EllipticArcPoint(center, radius, xAngle, s); |
| 176 | + |
| 177 | + while (remain > ZeroTolerance) |
| 178 | + { |
| 179 | + float step = (float)Math.Min(remain, Math.PI / 4); |
| 180 | + float signStep = step * sign; |
| 181 | + |
| 182 | + Vector2 p1 = prev; |
| 183 | + Vector2 p2 = EllipticArcPoint(center, radius, xAngle, s + signStep); |
| 184 | + |
| 185 | + float alphaT = (float)Math.Tan(signStep / 2); |
| 186 | + float alpha = (float)(Math.Sin(signStep) * (Math.Sqrt(4 + (3 * alphaT * alphaT)) - 1) / 3); |
| 187 | + Vector2 q1 = p1 + (alpha * EllipticArcDerivative(radius, xAngle, s)); |
| 188 | + Vector2 q2 = p2 - (alpha * EllipticArcDerivative(radius, xAngle, s + signStep)); |
| 189 | + |
| 190 | + ReadOnlySpan<PointF> bezierPoints = new CubicBezierLineSegment(from, q1, q2, p2).Flatten().Span; |
| 191 | + for (int i = 0; i < bezierPoints.Length; i++) |
| 192 | + { |
| 193 | + points.Add(bezierPoints[i]); |
| 194 | + } |
| 195 | + |
| 196 | + from = p2; |
| 197 | + |
| 198 | + s += signStep; |
| 199 | + remain -= step; |
| 200 | + prev = p2; |
| 201 | + } |
| 202 | + |
| 203 | + return points.ToArray(); |
| 204 | + } |
| 205 | + |
| 206 | + private static void EndpointToCenterArcParams( |
| 207 | + Vector2 p1, |
| 208 | + Vector2 p2, |
| 209 | + ref Vector2 r, |
| 210 | + float xRotation, |
| 211 | + bool flagA, |
| 212 | + bool flagS, |
| 213 | + out Vector2 center, |
| 214 | + out Vector2 angles) |
| 215 | + { |
| 216 | + double rX = Math.Abs(r.X); |
| 217 | + double rY = Math.Abs(r.Y); |
| 218 | + |
| 219 | + // (F.6.5.1) |
| 220 | + double dx2 = (p1.X - p2.X) / 2.0; |
| 221 | + double dy2 = (p1.Y - p2.Y) / 2.0; |
| 222 | + double x1p = (Math.Cos(xRotation) * dx2) + (Math.Sin(xRotation) * dy2); |
| 223 | + double y1p = (-Math.Sin(xRotation) * dx2) + (Math.Cos(xRotation) * dy2); |
| 224 | + |
| 225 | + // (F.6.5.2) |
| 226 | + double rxs = rX * rX; |
| 227 | + double rys = rY * rY; |
| 228 | + double x1ps = x1p * x1p; |
| 229 | + double y1ps = y1p * y1p; |
| 230 | + |
| 231 | + // check if the radius is too small `pq < 0`, when `dq > rxs * rys` (see below) |
| 232 | + // cr is the ratio (dq : rxs * rys) |
| 233 | + double cr = (x1ps / rxs) + (y1ps / rys); |
| 234 | + if (cr > 1) |
| 235 | + { |
| 236 | + // scale up rX,rY equally so cr == 1 |
| 237 | + double s = Math.Sqrt(cr); |
| 238 | + rX = s * rX; |
| 239 | + rY = s * rY; |
| 240 | + rxs = rX * rX; |
| 241 | + rys = rY * rY; |
| 242 | + } |
| 243 | + |
| 244 | + double dq = (rxs * y1ps) + (rys * x1ps); |
| 245 | + double pq = ((rxs * rys) - dq) / dq; |
| 246 | + double q = Math.Sqrt(Math.Max(0, pq)); // Use Max to account for float precision |
| 247 | + if (flagA == flagS) |
| 248 | + { |
| 249 | + q = -q; |
| 250 | + } |
| 251 | + |
| 252 | + double cxp = q * rX * y1p / rY; |
| 253 | + double cyp = -q * rY * x1p / rX; |
| 254 | + |
| 255 | + // (F.6.5.3) |
| 256 | + double cx = (Math.Cos(xRotation) * cxp) - (Math.Sin(xRotation) * cyp) + ((p1.X + p2.X) / 2); |
| 257 | + double cy = (Math.Sin(xRotation) * cxp) + (Math.Cos(xRotation) * cyp) + ((p1.Y + p2.Y) / 2); |
| 258 | + |
| 259 | + // (F.6.5.5) |
| 260 | + double theta = SvgAngle(1, 0, (x1p - cxp) / rX, (y1p - cyp) / rY); |
| 261 | + |
| 262 | + // (F.6.5.6) |
| 263 | + double delta = SvgAngle((x1p - cxp) / rX, (y1p - cyp) / rY, (-x1p - cxp) / rX, (-y1p - cyp) / rY); |
| 264 | + delta %= Math.PI * 2; |
| 265 | + |
| 266 | + if (!flagS && delta > 0) |
| 267 | + { |
| 268 | + delta -= 2 * Math.PI; |
| 269 | + } |
| 270 | + |
| 271 | + if (flagS && delta < 0) |
| 272 | + { |
| 273 | + delta += 2 * Math.PI; |
| 274 | + } |
| 275 | + |
| 276 | + r = new Vector2((float)rX, (float)rY); |
| 277 | + center = new Vector2((float)cx, (float)cy); |
| 278 | + angles = new Vector2((float)theta, (float)delta); |
| 279 | + } |
| 280 | + |
| 281 | + [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| 282 | + private static float Clamp(float val, float min, float max) |
| 283 | + { |
| 284 | + if (val < min) |
| 285 | + { |
| 286 | + return min; |
| 287 | + } |
| 288 | + else if (val > max) |
| 289 | + { |
| 290 | + return max; |
| 291 | + } |
| 292 | + else |
| 293 | + { |
| 294 | + return val; |
| 295 | + } |
| 296 | + } |
| 297 | + |
| 298 | + [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| 299 | + private static float SvgAngle(double ux, double uy, double vx, double vy) |
| 300 | + { |
| 301 | + var u = new Vector2((float)ux, (float)uy); |
| 302 | + var v = new Vector2((float)vx, (float)vy); |
| 303 | + |
| 304 | + // (F.6.5.4) |
| 305 | + float dot = Vector2.Dot(u, v); |
| 306 | + float len = u.Length() * v.Length(); |
| 307 | + float ang = (float)Math.Acos(Clamp(dot / len, -1, 1)); // floating point precision, slightly over values appear |
| 308 | + if (((u.X * v.Y) - (u.Y * v.X)) < 0) |
| 309 | + { |
| 310 | + ang = -ang; |
| 311 | + } |
| 312 | + |
| 313 | + return ang; |
| 314 | + } |
| 315 | + } |
| 316 | +} |
0 commit comments