Skip to content

Commit f3f1edf

Browse files
Merge pull request #206 from SixLabors/sw/path-drawer
Add drawing centric path building api.
2 parents 1a8dc88 + 4148163 commit f3f1edf

21 files changed

Lines changed: 740 additions & 416 deletions

src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ public void BeginText(FontRectangle bounds)
259259

260260
public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point)
261261
{
262-
this.builder.AddBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point);
262+
this.builder.AddCubicBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point);
263263
this.currentPoint = point;
264264
}
265265

@@ -418,7 +418,7 @@ public void MoveTo(Vector2 point)
418418

419419
public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point)
420420
{
421-
this.builder.AddBezier(this.currentPoint, secondControlPoint, point);
421+
this.builder.AddQuadraticBezier(this.currentPoint, secondControlPoint, point);
422422
this.currentPoint = point;
423423
}
424424

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
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+
}

src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,29 +60,13 @@ public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF control
6060
/// <summary>
6161
/// Gets the control points.
6262
/// </summary>
63-
/// <value>
64-
/// The control points.
65-
/// </value>
6663
public IReadOnlyList<PointF> ControlPoints => this.controlPoints;
6764

68-
/// <summary>
69-
/// Gets the end point.
70-
/// </summary>
71-
/// <value>
72-
/// The end point.
73-
/// </value>
65+
/// <inheritdoc/>
7466
public PointF EndPoint { get; }
7567

76-
/// <summary>
77-
/// Returns the current <see cref="ILineSegment" /> a simple linear path.
78-
/// </summary>
79-
/// <returns>
80-
/// Returns the current <see cref="ILineSegment" /> as simple linear path.
81-
/// </returns>
82-
public ReadOnlyMemory<PointF> Flatten()
83-
{
84-
return this.linePoints;
85-
}
68+
/// <inheritdoc/>
69+
public ReadOnlyMemory<PointF> Flatten() => this.linePoints;
8670

8771
/// <summary>
8872
/// Transforms the current LineSegment using specified matrix.
@@ -107,11 +91,7 @@ public CubicBezierLineSegment Transform(Matrix3x2 matrix)
10791
return new CubicBezierLineSegment(transformedPoints);
10892
}
10993

110-
/// <summary>
111-
/// Transforms the current LineSegment using specified matrix.
112-
/// </summary>
113-
/// <param name="matrix">The matrix.</param>
114-
/// <returns>A line segment with the matrix applied to it.</returns>
94+
/// <inheritdoc/>
11595
ILineSegment ILineSegment.Transform(Matrix3x2 matrix) => this.Transform(matrix);
11696

11797
private static PointF[] GetDrawingPoints(PointF[] controlPoints)

0 commit comments

Comments
 (0)