Skip to content

Commit acf438b

Browse files
authored
Merge pull request #99 from Voxel8/me/path_gradient_triangle_performance
Improve performance of path gradient brush on triangles.
2 parents 799d6af + ef5947d commit acf438b

10 files changed

Lines changed: 123 additions & 10 deletions

src/ImageSharp.Drawing/Processing/PathGradientBrush.cs

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ public sealed class PathGradientBrush : IBrush
2020
private readonly IList<Edge> edges;
2121

2222
private readonly Color centerColor;
23+
private readonly bool hasSpecialCenterColor;
2324

2425
/// <summary>
2526
/// Initializes a new instance of the <see cref="PathGradientBrush"/> class.
2627
/// </summary>
2728
/// <param name="points">Points that constitute a polygon that represents the gradient area.</param>
2829
/// <param name="colors">Array of colors that correspond to each point in the polygon.</param>
29-
/// <param name="centerColor">Color at the center of the gradient area to which the other colors converge.</param>
30-
public PathGradientBrush(PointF[] points, Color[] colors, Color centerColor)
30+
public PathGradientBrush(PointF[] points, Color[] colors)
3131
{
3232
if (points == null)
3333
{
@@ -62,7 +62,7 @@ public PathGradientBrush(PointF[] points, Color[] colors, Color centerColor)
6262
lines[i] = new LinearLineSegment(points[i % size], points[(i + 1) % size]);
6363
}
6464

65-
this.centerColor = centerColor;
65+
this.centerColor = CalculateCenterColor(colors);
6666

6767
Color ColorAt(int index) => colors[index % colors.Length];
6868

@@ -75,9 +75,12 @@ public PathGradientBrush(PointF[] points, Color[] colors, Color centerColor)
7575
/// </summary>
7676
/// <param name="points">Points that constitute a polygon that represents the gradient area.</param>
7777
/// <param name="colors">Array of colors that correspond to each point in the polygon.</param>
78-
public PathGradientBrush(PointF[] points, Color[] colors)
79-
: this(points, colors, CalculateCenterColor(colors))
78+
/// <param name="centerColor">Color at the center of the gradient area to which the other colors converge.</param>
79+
public PathGradientBrush(PointF[] points, Color[] colors, Color centerColor)
80+
: this(points, colors)
8081
{
82+
this.centerColor = centerColor;
83+
this.hasSpecialCenterColor = true;
8184
}
8285

8386
/// <inheritdoc />
@@ -88,7 +91,7 @@ public BrushApplicator<TPixel> CreateApplicator<TPixel>(
8891
RectangleF region)
8992
where TPixel : unmanaged, IPixel<TPixel>
9093
{
91-
return new PathGradientBrushApplicator<TPixel>(configuration, options, source, this.edges, this.centerColor);
94+
return new PathGradientBrushApplicator<TPixel>(configuration, options, source, this.edges, this.centerColor, this.hasSpecialCenterColor);
9295
}
9396

9497
private static Color CalculateCenterColor(Color[] colors)
@@ -209,6 +212,8 @@ private class PathGradientBrushApplicator<TPixel> : BrushApplicator<TPixel>
209212

210213
private readonly Vector4 centerColor;
211214

215+
private readonly bool hasSpecialCenterColor;
216+
212217
private readonly float maxDistance;
213218

214219
private readonly IList<Edge> edges;
@@ -225,19 +230,22 @@ private class PathGradientBrushApplicator<TPixel> : BrushApplicator<TPixel>
225230
/// <param name="source">The source image.</param>
226231
/// <param name="edges">Edges of the polygon.</param>
227232
/// <param name="centerColor">Color at the center of the gradient area to which the other colors converge.</param>
233+
/// <param name="hasSpecialCenterColor">Whether the center color is different from a smooth gradient between the edges.</param>
228234
public PathGradientBrushApplicator(
229235
Configuration configuration,
230236
GraphicsOptions options,
231237
ImageFrame<TPixel> source,
232238
IList<Edge> edges,
233-
Color centerColor)
239+
Color centerColor,
240+
bool hasSpecialCenterColor)
234241
: base(configuration, options, source)
235242
{
236243
this.edges = edges;
237244
PointF[] points = edges.Select(s => s.Start).ToArray();
238245

239246
this.center = points.Aggregate((p1, p2) => p1 + p2) / edges.Count;
240247
this.centerColor = (Vector4)centerColor;
248+
this.hasSpecialCenterColor = hasSpecialCenterColor;
241249
this.centerPixel = centerColor.ToPixel<TPixel>();
242250

243251
this.maxDistance = points.Select(p => (Vector2)(p - this.center)).Max(d => d.Length());
@@ -257,6 +265,27 @@ public PathGradientBrushApplicator(
257265
return this.centerPixel;
258266
}
259267

268+
if (this.edges.Count == 3 && !this.hasSpecialCenterColor)
269+
{
270+
if (!FindPointOnTriangle(
271+
this.edges[0].Start,
272+
this.edges[1].Start,
273+
this.edges[2].Start,
274+
point,
275+
out float u,
276+
out float v))
277+
{
278+
return this.transparentPixel;
279+
}
280+
281+
Vector4 pointColor = ((1 - u - v) * this.edges[0].StartColor) + (u * this.edges[0].EndColor) +
282+
(v * this.edges[2].StartColor);
283+
284+
TPixel px = default;
285+
px.FromScaledVector4(pointColor);
286+
return px;
287+
}
288+
260289
var direction = Vector2.Normalize(point - this.center);
261290
PointF end = point + (PointF)(direction * this.maxDistance);
262291

@@ -275,7 +304,9 @@ public PathGradientBrushApplicator(
275304

276305
var color = Vector4.Lerp(edgeColor, this.centerColor, ratio);
277306

278-
return new Color(color).ToPixel<TPixel>();
307+
TPixel pixel = default;
308+
pixel.FromScaledVector4(color);
309+
return pixel;
279310
}
280311
}
281312

@@ -301,6 +332,40 @@ public PathGradientBrushApplicator(
301332

302333
return closest;
303334
}
335+
336+
private static bool FindPointOnTriangle(PointF v1, PointF v2, PointF v3, PointF point, out float u, out float v)
337+
{
338+
Vector2 e1 = v2 - v1;
339+
Vector2 e2 = v3 - v2;
340+
Vector2 e3 = v1 - v3;
341+
342+
Vector2 pv1 = point - v1;
343+
Vector2 pv2 = point - v2;
344+
Vector2 pv3 = point - v3;
345+
346+
var d1 = Vector3.Cross(new Vector3(e1.X, e1.Y, 0), new Vector3(pv1.X, pv1.Y, 0));
347+
var d2 = Vector3.Cross(new Vector3(e2.X, e2.Y, 0), new Vector3(pv2.X, pv2.Y, 0));
348+
var d3 = Vector3.Cross(new Vector3(e3.X, e3.Y, 0), new Vector3(pv3.X, pv3.Y, 0));
349+
350+
if (Math.Sign(Vector3.Dot(d1, d2)) * Math.Sign(Vector3.Dot(d1, d3)) == -1 || Math.Sign(Vector3.Dot(d1, d2)) * Math.Sign(Vector3.Dot(d2, d3)) == -1)
351+
{
352+
u = 0;
353+
v = 0;
354+
return false;
355+
}
356+
357+
// From Real-Time Collision Detection
358+
// https://gamedev.stackexchange.com/questions/23743/whats-the-most-efficient-way-to-find-barycentric-coordinates
359+
float d00 = Vector2.Dot(e1, e1);
360+
float d01 = -Vector2.Dot(e1, e3);
361+
float d11 = Vector2.Dot(e3, e3);
362+
float d20 = Vector2.Dot(pv1, e1);
363+
float d21 = -Vector2.Dot(pv1, e3);
364+
float denominator = (d00 * d11) - (d01 * d01);
365+
u = ((d11 * d20) - (d01 * d21)) / denominator;
366+
v = ((d00 * d21) - (d01 * d20)) / denominator;
367+
return true;
368+
}
304369
}
305370
}
306371
}

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

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ public void FillRectangleWithDifferentColors<TPixel>(TestImageProvider<TPixel> p
3535
}
3636

3737
[Theory]
38-
[WithBlankImages(10, 10, PixelTypes.Rgba32)]
38+
[WithBlankImages(20, 20, PixelTypes.Rgba32)]
3939
public void FillTriangleWithDifferentColors<TPixel>(TestImageProvider<TPixel> provider)
4040
where TPixel : unmanaged, IPixel<TPixel>
4141
{
4242
provider.VerifyOperation(
4343
TolerantComparer,
4444
image =>
4545
{
46-
PointF[] points = { new PointF(5, 0), new PointF(10, 10), new PointF(0, 10) };
46+
PointF[] points = { new PointF(10, 0), new PointF(20, 20), new PointF(0, 20) };
4747
Color[] colors = { Color.Red, Color.Green, Color.Blue };
4848

4949
var brush = new PathGradientBrush(points, colors);
@@ -53,6 +53,54 @@ public void FillTriangleWithDifferentColors<TPixel>(TestImageProvider<TPixel> pr
5353
});
5454
}
5555

56+
[Theory]
57+
[WithBlankImages(20, 20, PixelTypes.HalfSingle)]
58+
public void FillTriangleWithGreyscale<TPixel>(TestImageProvider<TPixel> provider)
59+
where TPixel : unmanaged, IPixel<TPixel>
60+
{
61+
provider.VerifyOperation(
62+
ImageComparer.TolerantPercentage(0.02f),
63+
image =>
64+
{
65+
PointF[] points = { new PointF(10, 0), new PointF(20, 20), new PointF(0, 20) };
66+
67+
var c1 = new Rgba32();
68+
var c2 = new Rgba32();
69+
var c3 = new Rgba32();
70+
new HalfSingle(-1).ToRgba32(ref c1);
71+
new HalfSingle(0).ToRgba32(ref c2);
72+
new HalfSingle(1).ToRgba32(ref c3);
73+
74+
Color[] colors = { new Color(c1), new Color(c2), new Color(c3) };
75+
76+
var brush = new PathGradientBrush(points, colors);
77+
78+
image.Mutate(x => x.Fill(brush));
79+
image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
80+
});
81+
}
82+
83+
84+
[Theory]
85+
[WithBlankImages(20, 20, PixelTypes.Rgba32)]
86+
public void FillTriangleWithDifferentColorsCenter<TPixel>(TestImageProvider<TPixel> provider)
87+
where TPixel : unmanaged, IPixel<TPixel>
88+
{
89+
provider.VerifyOperation(
90+
TolerantComparer,
91+
image =>
92+
{
93+
PointF[] points = { new PointF(10, 0), new PointF(20, 20), new PointF(0, 20) };
94+
Color[] colors = { Color.Red, Color.Green, Color.Blue };
95+
96+
var brush = new PathGradientBrush(points, colors, Color.White);
97+
98+
image.Mutate(x => x.Fill(brush));
99+
image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
100+
});
101+
}
102+
103+
56104
[Theory]
57105
[WithBlankImages(10, 10, PixelTypes.Rgba32)]
58106
public void FillRectangleWithSingleColor<TPixel>(TestImageProvider<TPixel> provider)
494 Bytes
Loading
724 Bytes
Loading
Loading
729 Bytes
Loading
424 Bytes
Loading
424 Bytes
Loading
112 Bytes
Loading

0 commit comments

Comments
 (0)