44namespace 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>
1110public 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}
0 commit comments