Skip to content

Commit 90e727c

Browse files
committed
simple intersection calc in PathGradientBrush
1 parent 9b6e55f commit 90e727c

4 files changed

Lines changed: 48 additions & 105 deletions

File tree

src/ImageSharp.Drawing/Processing/PathGradientBrush.cs

Lines changed: 41 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using System.Numerics;
99
using System.Threading;
10+
using SixLabors.ImageSharp.Drawing.Utilities;
1011
using SixLabors.ImageSharp.Memory;
1112
using SixLabors.ImageSharp.PixelFormats;
1213

@@ -17,7 +18,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing
1718
/// </summary>
1819
public sealed class PathGradientBrush : IBrush
1920
{
20-
private readonly IList<Edge> edges;
21+
private readonly Edge[] edges;
2122
private readonly Color centerColor;
2223
private readonly bool hasSpecialCenterColor;
2324

@@ -54,19 +55,16 @@ public PathGradientBrush(PointF[] points, Color[] colors)
5455

5556
int size = points.Length;
5657

57-
var lines = new ILineSegment[size];
58+
this.edges = new Edge[points.Length];
5859

59-
for (int i = 0; i < size; i++)
60+
for (int i = 0; i < points.Length; i++)
6061
{
61-
lines[i] = new LinearLineSegment(points[i % size], points[(i + 1) % size]);
62+
this.edges[i] = new Edge(points[i % size], points[(i + 1) % size], ColorAt(i), ColorAt(i + 1));
6263
}
6364

6465
this.centerColor = CalculateCenterColor(colors);
6566

6667
Color ColorAt(int index) => colors[index % colors.Length];
67-
68-
this.edges = lines.Select(s => new Path(s))
69-
.Select((path, i) => new Edge(path, ColorAt(i), ColorAt(i + 1))).ToList();
7068
}
7169

7270
/// <summary>
@@ -136,65 +134,29 @@ private class Edge
136134
{
137135
private readonly float length;
138136

139-
public Edge(Path path, Color startColor, Color endColor)
137+
public Edge(Vector2 start, Vector2 end, Color startColor, Color endColor)
140138
{
141-
this.Path = path;
142-
143-
Vector2[] points = path.LineSegments.SelectMany(s => s.Flatten().ToArray()).Select(p => (Vector2)p).ToArray();
144-
145-
this.Start = points[0];
139+
this.Start = start;
140+
this.End = end;
146141
this.StartColor = (Vector4)startColor;
147-
148-
this.End = points.Last();
149142
this.EndColor = (Vector4)endColor;
150143

151144
this.length = DistanceBetween(this.End, this.Start);
152145
}
153146

154-
public Path Path { get; }
147+
public Vector2 Start { get; }
155148

156-
public PointF Start { get; }
149+
public Vector2 End { get; }
157150

158151
public Vector4 StartColor { get; }
159152

160-
public PointF End { get; }
161-
162153
public Vector4 EndColor { get; }
163154

164-
public Intersection? FindIntersection(
165-
PointF start,
166-
PointF end,
167-
Span<PointF> intersections,
168-
Span<PointOrientation> orientations)
169-
{
170-
int pathIntersections = this.Path.FindIntersections(
171-
start,
172-
end,
173-
intersections.Slice(0, this.Path.MaxIntersections),
174-
orientations.Slice(0, this.Path.MaxIntersections));
175-
176-
if (pathIntersections == 0)
177-
{
178-
return null;
179-
}
180-
181-
intersections = intersections.Slice(0, pathIntersections);
182-
183-
PointF minPoint = intersections[0];
184-
var min = new Intersection(minPoint, ((Vector2)(minPoint - start)).LengthSquared());
185-
for (int i = 1; i < intersections.Length; i++)
186-
{
187-
PointF point = intersections[i];
188-
var current = new Intersection(point, ((Vector2)(point - start)).LengthSquared());
189-
190-
if (min.Distance > current.Distance)
191-
{
192-
min = current;
193-
}
194-
}
195-
196-
return min;
197-
}
155+
public bool Intersect(
156+
Vector2 start,
157+
Vector2 end,
158+
ref Vector2 ip) =>
159+
Utilities.Intersect.LineSegmentToLineSegmentIgnoreCollinear(start, end, this.Start, this.End, ref ip);
198160

199161
public Vector4 ColorAt(float distance)
200162
{
@@ -213,7 +175,7 @@ public Vector4 ColorAt(float distance)
213175
private sealed class PathGradientBrushApplicator<TPixel> : BrushApplicator<TPixel>
214176
where TPixel : unmanaged, IPixel<TPixel>
215177
{
216-
private readonly PointF center;
178+
private readonly Vector2 center;
217179

218180
private readonly Vector4 centerColor;
219181

@@ -227,12 +189,6 @@ private sealed class PathGradientBrushApplicator<TPixel> : BrushApplicator<TPixe
227189

228190
private readonly TPixel transparentPixel;
229191

230-
private readonly MemoryAllocator allocator;
231-
232-
private readonly int maxIntersections;
233-
234-
private readonly int scalineWidth;
235-
236192
private readonly ThreadLocal<ThreadContextData> threadContextData;
237193

238194
private bool isDisposed;
@@ -256,28 +212,24 @@ public PathGradientBrushApplicator(
256212
: base(configuration, options, source)
257213
{
258214
this.edges = edges;
259-
PointF[] points = edges.Select(s => s.Start).ToArray();
215+
Vector2[] points = edges.Select(s => s.Start).ToArray();
260216

261217
this.center = points.Aggregate((p1, p2) => p1 + p2) / edges.Count;
262218
this.centerColor = (Vector4)centerColor;
263219
this.hasSpecialCenterColor = hasSpecialCenterColor;
264220
this.centerPixel = centerColor.ToPixel<TPixel>();
265221
this.maxDistance = points.Select(p => (Vector2)(p - this.center)).Max(d => d.Length());
266222
this.transparentPixel = Color.Transparent.ToPixel<TPixel>();
267-
268-
this.scalineWidth = source.Width;
269-
this.maxIntersections = this.edges.Max(e => e.Path.MaxIntersections);
270-
this.allocator = configuration.MemoryAllocator;
271223
this.threadContextData = new ThreadLocal<ThreadContextData>(
272-
() => new ThreadContextData(this.allocator, this.scalineWidth, this.maxIntersections),
224+
() => new ThreadContextData(configuration.MemoryAllocator, source.Width),
273225
true);
274226
}
275227

276-
internal TPixel this[int x, int y, Span<PointF> intersections, Span<PointOrientation> orientations]
228+
internal TPixel this[int x, int y]
277229
{
278230
get
279231
{
280-
var point = new PointF(x, y);
232+
var point = new Vector2(x, y);
281233

282234
if (point == this.center)
283235
{
@@ -307,17 +259,17 @@ public PathGradientBrushApplicator(
307259
}
308260

309261
var direction = Vector2.Normalize(point - this.center);
310-
PointF end = point + (PointF)(direction * this.maxDistance);
262+
Vector2 end = point + (direction * this.maxDistance);
311263

312-
(Edge edge, Intersection? info) = this.FindIntersection(point, end, intersections, orientations);
264+
(Edge edge, Vector2 point)? isc = this.FindIntersection(point, end);
313265

314-
if (!info.HasValue)
266+
if (!isc.HasValue)
315267
{
316268
return this.transparentPixel;
317269
}
318270

319-
PointF intersection = info.Value.Point;
320-
Vector4 edgeColor = edge.ColorAt(intersection);
271+
Vector2 intersection = isc.Value.point;
272+
Vector4 edgeColor = isc.Value.edge.ColorAt(intersection);
321273

322274
float length = DistanceBetween(intersection, this.center);
323275
float ratio = length > 0 ? DistanceBetween(intersection, point) / length : 0;
@@ -336,8 +288,6 @@ public override void Apply(Span<float> scanline, int x, int y)
336288
ThreadContextData contextData = this.threadContextData.Value;
337289
Span<float> amounts = contextData.AmountSpan.Slice(0, scanline.Length);
338290
Span<TPixel> overlays = contextData.OverlaySpan.Slice(0, scanline.Length);
339-
Span<PointF> intersections = contextData.IntersectionsSpan;
340-
Span<PointOrientation> orientations = contextData.OrientationsSpan;
341291
float blendPercentage = this.Options.BlendPercentage;
342292

343293
// TODO: Remove bounds checks.
@@ -346,15 +296,15 @@ public override void Apply(Span<float> scanline, int x, int y)
346296
for (int i = 0; i < scanline.Length; i++)
347297
{
348298
amounts[i] = scanline[i] * blendPercentage;
349-
overlays[i] = this[x + i, y, intersections, orientations];
299+
overlays[i] = this[x + i, y];
350300
}
351301
}
352302
else
353303
{
354304
for (int i = 0; i < scanline.Length; i++)
355305
{
356306
amounts[i] = scanline[i];
357-
overlays[i] = this[x + i, y, intersections, orientations];
307+
overlays[i] = this[x + i, y];
358308
}
359309
}
360310

@@ -385,33 +335,33 @@ protected override void Dispose(bool disposing)
385335
this.isDisposed = true;
386336
}
387337

388-
private (Edge edge, Intersection? info) FindIntersection(
338+
private (Edge edge, Vector2 point)? FindIntersection(
389339
PointF start,
390-
PointF end,
391-
Span<PointF> intersections,
392-
Span<PointOrientation> orientations)
340+
PointF end)
393341
{
394-
(Edge edge, Intersection? info) closest = default;
395-
342+
Vector2 ip = default;
343+
Vector2 closestIntersection = default;
344+
Edge closestEdge = null;
345+
float minDistance = float.MaxValue;
396346
foreach (Edge edge in this.edges)
397347
{
398-
Intersection? intersection = edge.FindIntersection(start, end, intersections, orientations);
399-
400-
if (!intersection.HasValue)
348+
if (!edge.Intersect(start, end, ref ip))
401349
{
402350
continue;
403351
}
404352

405-
if (closest.info == null || closest.info.Value.Distance > intersection.Value.Distance)
353+
float d = Vector2.DistanceSquared(start, end);
354+
if (d < minDistance)
406355
{
407-
closest = (edge, intersection);
356+
closestEdge = edge;
357+
closestIntersection = ip;
408358
}
409359
}
410360

411-
return closest;
361+
return closestEdge != null ? (closestEdge, closestIntersection) : ((Edge edge, Vector2 point)?)null;
412362
}
413363

414-
private static bool FindPointOnTriangle(PointF v1, PointF v2, PointF v3, PointF point, out float u, out float v)
364+
private static bool FindPointOnTriangle(Vector2 v1, Vector2 v2, Vector2 v3, Vector2 point, out float u, out float v)
415365
{
416366
Vector2 e1 = v2 - v1;
417367
Vector2 e2 = v3 - v2;
@@ -450,34 +400,24 @@ private sealed class ThreadContextData : IDisposable
450400
private bool isDisposed;
451401
private readonly IMemoryOwner<float> amountBuffer;
452402
private readonly IMemoryOwner<TPixel> overlayBuffer;
453-
private readonly IMemoryOwner<PointF> intersectionsBuffer;
454-
private readonly IMemoryOwner<PointOrientation> orientationsBuffer;
455403

456-
public ThreadContextData(MemoryAllocator allocator, int scanlineLength, int maxIntersections)
404+
public ThreadContextData(MemoryAllocator allocator, int scanlineLength)
457405
{
458406
this.amountBuffer = allocator.Allocate<float>(scanlineLength);
459407
this.overlayBuffer = allocator.Allocate<TPixel>(scanlineLength);
460-
this.intersectionsBuffer = allocator.Allocate<PointF>(maxIntersections);
461-
this.orientationsBuffer = allocator.Allocate<PointOrientation>(maxIntersections);
462408
}
463409

464410
public Span<float> AmountSpan => this.amountBuffer.Memory.Span;
465411

466412
public Span<TPixel> OverlaySpan => this.overlayBuffer.Memory.Span;
467413

468-
public Span<PointF> IntersectionsSpan => this.intersectionsBuffer.Memory.Span;
469-
470-
public Span<PointOrientation> OrientationsSpan => this.orientationsBuffer.Memory.Span;
471-
472414
public void Dispose()
473415
{
474416
if (!this.isDisposed)
475417
{
476418
this.isDisposed = true;
477419
this.amountBuffer.Dispose();
478420
this.overlayBuffer.Dispose();
479-
this.intersectionsBuffer.Dispose();
480-
this.orientationsBuffer.Dispose();
481421
}
482422
}
483423
}

src/ImageSharp.Drawing/Utilities/Intersect.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

4+
using System.Numerics;
5+
46
namespace SixLabors.ImageSharp.Drawing.Utilities
57
{
68
internal static class Intersect
@@ -9,7 +11,7 @@ internal static class Intersect
911
private const float MinusEps = -Eps;
1012
private const float OnePlusEps = 1 + Eps;
1113

12-
public static bool LineSegmentToLineSegmentIgnoreCollinear(PointF a0, PointF a1, PointF b0, PointF b1, ref PointF intersectionPoint)
14+
public static bool LineSegmentToLineSegmentIgnoreCollinear(Vector2 a0, Vector2 a1, Vector2 b0, Vector2 b1, ref Vector2 intersectionPoint)
1315
{
1416
float dax = a1.X - a0.X;
1517
float day = a1.Y - a0.Y;

tests/ImageSharp.Drawing.Tests/Drawing/FillPathGradientBrushTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ public void ShouldThrowArgumentOutOfRangeExceptionWhenEmptyColorArrayIsGiven()
191191
public void FillComplex<TPixel>(TestImageProvider<TPixel> provider)
192192
where TPixel : unmanaged, IPixel<TPixel>
193193
=> provider.VerifyOperation(
194-
TolerantComparer,
194+
new TolerantImageComparer(0.2f),
195195
image =>
196196
{
197197
var star = new Star(50, 50, 5, 20, 45);

tests/ImageSharp.Drawing.Tests/Utilities/IntersectTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

4+
using System.Numerics;
45
using SixLabors.ImageSharp.Drawing.Utilities;
56
using Xunit;
67

@@ -31,7 +32,7 @@ public void LineSegmentToLineSegmentNoCollinear(
3132
(float x, float y) b1,
3233
(float x, float y)? expected)
3334
{
34-
PointF ip = default;
35+
Vector2 ip = default;
3536

3637
bool result = Intersect.LineSegmentToLineSegmentIgnoreCollinear(P(a0), P(a1), P(b0), P(b1), ref ip);
3738
Assert.Equal(result, expected.HasValue);
@@ -40,7 +41,7 @@ public void LineSegmentToLineSegmentNoCollinear(
4041
Assert.Equal(P(expected.Value), ip, new ApproximateFloatComparer(1e-3f));
4142
}
4243

43-
static PointF P((float x, float y) p) => new PointF(p.x, p.y);
44+
static Vector2 P((float x, float y) p) => new Vector2(p.x, p.y);
4445
}
4546
}
4647
}

0 commit comments

Comments
 (0)