Skip to content

Commit 09c1f64

Browse files
Update brushes to handle ColrV1 spec
1 parent fa73c5c commit 09c1f64

File tree

7 files changed

+334
-129
lines changed

7 files changed

+334
-129
lines changed

src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,11 @@ public RadialGradientBrushApplicator(
109109
this.center = center;
110110
this.referenceAxisEnd = referenceAxisEnd;
111111
this.axisRatio = axisRatio;
112-
this.rotation = RadialGradientBrushApplicator<TPixel>.AngleBetween(
112+
this.rotation = AngleBetween(
113113
this.center,
114114
new PointF(this.center.X + 1, this.center.Y),
115115
this.referenceAxisEnd);
116-
this.referenceRadius = RadialGradientBrushApplicator<TPixel>.DistanceBetween(this.center, this.referenceAxisEnd);
116+
this.referenceRadius = DistanceBetween(this.center, this.referenceAxisEnd);
117117
this.secondRadius = this.referenceRadius * this.axisRatio;
118118

119119
this.referenceRadiusSquared = this.referenceRadius * this.referenceRadius;

src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs

Lines changed: 137 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,57 @@
44
namespace SixLabors.ImageSharp.Drawing.Processing;
55

66
/// <summary>
7-
/// Provides an implementation of a brush for painting linear gradients within areas.
8-
/// Supported right now:
9-
/// - a set of colors in relative distances to each other.
7+
/// Provides a brush that paints linear gradients within an area.
8+
/// Supports both classic two-point gradients and three-point (rotated) gradients.
109
/// </summary>
1110
public sealed class LinearGradientBrush : GradientBrush
1211
{
13-
private readonly PointF p1;
14-
private readonly PointF p2;
12+
private readonly PointF startPoint;
13+
private readonly PointF endPoint;
14+
private readonly PointF? rotationPoint;
1515

1616
/// <summary>
17-
/// Initializes a new instance of the <see cref="LinearGradientBrush"/> class.
17+
/// Initializes a new instance of the <see cref="LinearGradientBrush"/> class using
18+
/// a start and end point.
1819
/// </summary>
19-
/// <param name="p1">Start point</param>
20-
/// <param name="p2">End point</param>
21-
/// <param name="repetitionMode">defines how colors are repeated.</param>
22-
/// <param name="colorStops"><inheritdoc /></param>
20+
/// <param name="p0">The start point of the gradient.</param>
21+
/// <param name="p1">The end point of the gradient.</param>
22+
/// <param name="repetitionMode">Defines how the colors are repeated.</param>
23+
/// <param name="colorStops">The ordered color stops of the gradient.</param>
2324
public LinearGradientBrush(
25+
PointF p0,
2426
PointF p1,
25-
PointF p2,
2627
GradientRepetitionMode repetitionMode,
2728
params ColorStop[] colorStops)
2829
: base(repetitionMode, colorStops)
2930
{
30-
this.p1 = p1;
31-
this.p2 = p2;
31+
this.startPoint = p0;
32+
this.endPoint = p1;
33+
this.rotationPoint = null;
34+
}
35+
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="LinearGradientBrush"/> class using
38+
/// three points to define a rotated gradient axis.
39+
/// </summary>
40+
/// <param name="p0">The first point (start of the gradient).</param>
41+
/// <param name="p1">The second point (gradient vector endpoint).</param>
42+
/// <param name="rotationPoint">
43+
/// The rotation reference point. This defines the rotation of the gradient axis.
44+
/// </param>
45+
/// <param name="repetitionMode">Defines how the colors are repeated.</param>
46+
/// <param name="colorStops">The ordered color stops of the gradient.</param>
47+
public LinearGradientBrush(
48+
PointF p0,
49+
PointF p1,
50+
PointF rotationPoint,
51+
GradientRepetitionMode repetitionMode,
52+
params ColorStop[] colorStops)
53+
: base(repetitionMode, colorStops)
54+
{
55+
this.startPoint = p0;
56+
this.endPoint = p1;
57+
this.rotationPoint = rotationPoint;
3258
}
3359

3460
/// <inheritdoc/>
@@ -37,135 +63,165 @@ public override bool Equals(Brush? other)
3763
if (other is LinearGradientBrush brush)
3864
{
3965
return base.Equals(other)
40-
&& this.p1.Equals(brush.p1)
41-
&& this.p2.Equals(brush.p2);
66+
&& this.startPoint.Equals(brush.startPoint)
67+
&& this.endPoint.Equals(brush.endPoint)
68+
&& Nullable.Equals(this.rotationPoint, brush.rotationPoint);
4269
}
4370

4471
return false;
4572
}
4673

47-
/// <inheritdoc />
74+
/// <inheritdoc/>
75+
public override int GetHashCode()
76+
=> HashCode.Combine(base.GetHashCode(), this.startPoint, this.endPoint, this.rotationPoint);
77+
78+
/// <inheritdoc/>
4879
public override BrushApplicator<TPixel> CreateApplicator<TPixel>(
4980
Configuration configuration,
5081
GraphicsOptions options,
5182
ImageFrame<TPixel> source,
52-
RectangleF region) =>
53-
new LinearGradientBrushApplicator<TPixel>(
83+
RectangleF region)
84+
=> new LinearGradientBrushApplicator<TPixel>(
5485
configuration,
5586
options,
5687
source,
57-
this.p1,
58-
this.p2,
88+
this.startPoint,
89+
this.endPoint,
90+
this.rotationPoint,
5991
this.ColorStops,
6092
this.RepetitionMode);
6193

62-
/// <inheritdoc/>
63-
public override int GetHashCode()
64-
=> HashCode.Combine(base.GetHashCode(), this.p1, this.p2);
65-
6694
/// <summary>
67-
/// The linear gradient brush applicator.
95+
/// Implements the gradient application logic for <see cref="LinearGradientBrush"/>.
6896
/// </summary>
6997
/// <typeparam name="TPixel">The pixel format.</typeparam>
7098
private sealed class LinearGradientBrushApplicator<TPixel> : GradientBrushApplicator<TPixel>
7199
where TPixel : unmanaged, IPixel<TPixel>
72100
{
73101
private readonly PointF start;
74-
75102
private readonly PointF end;
76-
77-
/// <summary>
78-
/// the vector along the gradient, x component
79-
/// </summary>
80103
private readonly float alongX;
81-
82-
/// <summary>
83-
/// the vector along the gradient, y component
84-
/// </summary>
85104
private readonly float alongY;
86-
87-
/// <summary>
88-
/// the vector perpendicular to the gradient, y component
89-
/// </summary>
90-
private readonly float acrossY;
91-
92-
/// <summary>
93-
/// the vector perpendicular to the gradient, x component
94-
/// </summary>
95105
private readonly float acrossX;
96-
97-
/// <summary>
98-
/// the result of <see cref="alongX"/>^2 + <see cref="alongY"/>^2
99-
/// </summary>
106+
private readonly float acrossY;
100107
private readonly float alongsSquared;
101-
102-
/// <summary>
103-
/// the length of the defined gradient (between source and end)
104-
/// </summary>
105108
private readonly float length;
106109

107110
/// <summary>
108-
/// Initializes a new instance of the <see cref="LinearGradientBrushApplicator{TPixel}" /> class.
111+
/// Initializes a new instance of the <see cref="LinearGradientBrushApplicator{TPixel}"/> class.
109112
/// </summary>
110-
/// <param name="configuration">The configuration instance to use when performing operations.</param>
113+
/// <param name="configuration">The ImageSharp configuration.</param>
111114
/// <param name="options">The graphics options.</param>
112-
/// <param name="source">The source image.</param>
113-
/// <param name="start">The start point of the gradient.</param>
114-
/// <param name="end">The end point of the gradient.</param>
115-
/// <param name="colorStops">A tuple list of colors and their respective position between 0 and 1 on the line.</param>
116-
/// <param name="repetitionMode">Defines how the gradient colors are repeated.</param>
115+
/// <param name="source">The target image frame.</param>
116+
/// <param name="p0">The gradient start point.</param>
117+
/// <param name="p1">The gradient end point.</param>
118+
/// <param name="p2">The optional rotation point.</param>
119+
/// <param name="colorStops">The gradient color stops.</param>
120+
/// <param name="repetitionMode">Defines how the gradient repeats.</param>
117121
public LinearGradientBrushApplicator(
118122
Configuration configuration,
119123
GraphicsOptions options,
120124
ImageFrame<TPixel> source,
121-
PointF start,
122-
PointF end,
125+
PointF p0,
126+
PointF p1,
127+
PointF? p2,
123128
ColorStop[] colorStops,
124129
GradientRepetitionMode repetitionMode)
125130
: base(configuration, options, source, colorStops, repetitionMode)
126131
{
127-
this.start = start;
128-
this.end = end;
132+
// Determine whether this is a simple linear gradient (2 points)
133+
// or a rotated one (3 points).
134+
if (p2 is null)
135+
{
136+
// Classic SVG-style gradient from start -> end.
137+
this.start = p0;
138+
this.end = p1;
139+
}
140+
else
141+
{
142+
// Compute the rotated gradient axis per COLRv1 rules.
143+
// p0 = start, p1 = gradient vector, p2 = rotation reference.
144+
float vx = p1.X - p0.X;
145+
float vy = p1.Y - p0.Y;
146+
float rx = p2.Value.X - p0.X;
147+
float ry = p2.Value.Y - p0.Y;
148+
149+
// n = perpendicular to rotation vector
150+
float nx = ry;
151+
float ny = -rx;
152+
153+
// Avoid divide-by-zero if p0 == p2.
154+
float ndotn = (nx * nx) + (ny * ny);
155+
if (ndotn == 0f)
156+
{
157+
this.start = p0;
158+
this.end = p1;
159+
}
160+
else
161+
{
162+
// Project p1 - p0 onto perpendicular direction.
163+
float vdotn = (vx * nx) + (vy * ny);
164+
float scale = vdotn / ndotn;
165+
166+
// The derived endpoint after rotation.
167+
this.start = p0;
168+
this.end = new PointF(p0.X + (scale * nx), p0.Y + (scale * ny));
169+
}
170+
}
129171

130-
// the along vector:
172+
// Calculate axis vectors.
131173
this.alongX = this.end.X - this.start.X;
132174
this.alongY = this.end.Y - this.start.Y;
133175

134-
// the cross vector:
176+
// Perpendicular axis vector.
135177
this.acrossX = this.alongY;
136178
this.acrossY = -this.alongX;
137179

138-
// some helpers:
180+
// Precompute squared length and actual length for later use.
139181
this.alongsSquared = (this.alongX * this.alongX) + (this.alongY * this.alongY);
140182
this.length = MathF.Sqrt(this.alongsSquared);
141183
}
142184

185+
/// <inheritdoc/>
143186
protected override float PositionOnGradient(float x, float y)
144187
{
145-
if (this.acrossX == 0)
188+
// Degenerate case: gradient length == 0, use final stop color.
189+
if (this.alongsSquared == 0f)
146190
{
147-
return (x - this.start.X) / (this.end.X - this.start.X);
191+
return 1f;
148192
}
149-
else if (this.acrossY == 0)
193+
194+
// Fast path for horizontal gradients.
195+
if (this.acrossX == 0f)
150196
{
151-
return (y - this.start.Y) / (this.end.Y - this.start.Y);
197+
float denom = this.end.X - this.start.X;
198+
return denom != 0f ? (x - this.start.X) / denom : 1f;
152199
}
153-
else
200+
201+
// Fast path for vertical gradients.
202+
if (this.acrossY == 0f)
154203
{
155-
float deltaX = x - this.start.X;
156-
float deltaY = y - this.start.Y;
157-
float k = ((this.alongY * deltaX) - (this.alongX * deltaY)) / this.alongsSquared;
204+
float denom = this.end.Y - this.start.Y;
205+
return denom != 0f ? (y - this.start.Y) / denom : 1f;
206+
}
158207

159-
// point on the line:
160-
float x4 = x - (k * this.alongY);
161-
float y4 = y + (k * this.alongX);
208+
// General case: project sample point onto the gradient axis.
209+
float deltaX = x - this.start.X;
210+
float deltaY = y - this.start.Y;
162211

163-
// get distance from (x4,y4) to start
164-
float distance = MathF.Sqrt(MathF.Pow(x4 - this.start.X, 2) + MathF.Pow(y4 - this.start.Y, 2));
212+
// Compute perpendicular projection scalar.
213+
float k = ((this.alongY * deltaX) - (this.alongX * deltaY)) / this.alongsSquared;
165214

166-
// get and return ratio
167-
return distance / this.length;
168-
}
215+
// Determine projected point on the gradient line.
216+
float projX = x - (k * this.alongY);
217+
float projY = y + (k * this.alongX);
218+
219+
// Compute distance from gradient start to projected point.
220+
float dx = projX - this.start.X;
221+
float dy = projY - this.start.Y;
222+
223+
// Normalize to [0,1] range along the gradient length.
224+
return this.length > 0f ? MathF.Sqrt((dx * dx) + (dy * dy)) / this.length : 1f;
169225
}
170226
}
171227
}

src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.Brushes.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,24 @@ private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matr
6363

6464
PointF p0 = paint.P0;
6565
PointF p1 = paint.P1;
66+
PointF? p2 = paint.P2;
6667

6768
// Apply any transform defined on the paint.
6869
if (!transform.IsIdentity)
6970
{
7071
p0 = Vector2.Transform(p0, transform);
7172
p1 = Vector2.Transform(p1, transform);
73+
74+
if (p2.HasValue)
75+
{
76+
p2 = Vector2.Transform(p2.Value, transform);
77+
}
78+
}
79+
80+
if (p2.HasValue)
81+
{
82+
brush = new LinearGradientBrush(p0, p1, p2.Value, mode, stops);
83+
return true;
7284
}
7385

7486
brush = new LinearGradientBrush(p0, p1, mode, stops);
@@ -91,20 +103,23 @@ private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matr
91103
GradientRepetitionMode mode = MapSpread(paint.Spread);
92104

93105
// Apply any transform defined on the paint.
94-
PointF center = paint.Center;
106+
PointF center0 = paint.Center0;
107+
PointF center1 = paint.Center1;
95108
if (!transform.IsIdentity)
96109
{
97-
center = Vector2.Transform(center, transform);
110+
center0 = Vector2.Transform(center0, transform);
111+
center1 = Vector2.Transform(center1, transform);
98112
}
99113

100-
brush = new RadialGradientBrush(center, paint.Radius, mode, stops);
114+
brush = new RadialGradientBrush(center0, paint.Radius0, center1, paint.Radius1, mode, stops);
101115
return true;
102116
}
103117

104118
/// <summary>
105119
/// Creates a <see cref="SweepGradientBrush"/> from a <see cref="SweepGradientPaint"/>.
106120
/// </summary>
107121
/// <param name="paint">The sweep gradient paint.</param>
122+
/// <param name="transform">The transform to apply to the gradient center point.</param>
108123
/// <param name="brush">The resulting brush.</param>
109124
/// <returns><see langword="true"/> if created; otherwise, <see langword="false"/>.</returns>
110125
private static bool TryCreateSweepGradientBrush(SweepGradientPaint paint, Matrix3x2 transform, out Brush? brush)

0 commit comments

Comments
 (0)