Skip to content

Commit dab4a36

Browse files
authored
Merge pull request SixLabors#542 from jongleur1983/GradientBrush
Gradient Brushes
2 parents 9ae3920 + 0b8f34e commit dab4a36

File tree

11 files changed

+1246
-1
lines changed

11 files changed

+1246
-1
lines changed

src/ImageSharp.Drawing/Processing/Drawing/Brushes/BrushApplicator.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ internal virtual void Apply(Span<float> scanline, int x, int y)
7979
{
8080
amountSpan[i] = scanline[i] * this.Options.BlendPercentage;
8181
}
82+
else
83+
{
84+
amountSpan[i] = scanline[i];
85+
}
8286

8387
overlaySpan[i] = this[x + i, y];
8488
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Diagnostics;
2+
3+
using SixLabors.ImageSharp.PixelFormats;
4+
5+
namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes
6+
{
7+
/// <summary>
8+
/// A struct that defines a single color stop.
9+
/// </summary>
10+
/// <typeparam name="TPixel">The pixel format.</typeparam>
11+
[DebuggerDisplay("ColorStop({Ratio} -> {Color}")]
12+
public struct ColorStop<TPixel>
13+
where TPixel : struct, IPixel<TPixel>
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="ColorStop{TPixel}" /> struct.
17+
/// </summary>
18+
/// <param name="ratio">Where should it be? 0 is at the start, 1 at the end of the Gradient.</param>
19+
/// <param name="color">What color should be used at that point?</param>
20+
public ColorStop(float ratio, TPixel color)
21+
{
22+
this.Ratio = ratio;
23+
this.Color = color;
24+
}
25+
26+
/// <summary>
27+
/// Gets the point along the defined gradient axis.
28+
/// </summary>
29+
public float Ratio { get; }
30+
31+
/// <summary>
32+
/// Gets the color to be used.
33+
/// </summary>
34+
public TPixel Color { get; }
35+
}
36+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
using System;
2+
3+
using SixLabors.ImageSharp.PixelFormats;
4+
using SixLabors.Primitives;
5+
6+
namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes
7+
{
8+
/// <summary>
9+
/// Gradient Brush with elliptic shape.
10+
/// The ellipse is defined by a center point,
11+
/// a point on the longest extension of the ellipse and
12+
/// the ratio between longest and shortest extension.
13+
/// </summary>
14+
/// <typeparam name="TPixel">The Pixel format that is used.</typeparam>
15+
public sealed class EllipticGradientBrush<TPixel> : GradientBrushBase<TPixel>
16+
where TPixel : struct, IPixel<TPixel>
17+
{
18+
private readonly Point center;
19+
20+
private readonly Point referenceAxisEnd;
21+
22+
private readonly float axisRatio;
23+
24+
/// <inheritdoc cref="GradientBrushBase{TPixel}" />
25+
/// <param name="center">The center of the elliptical gradient and 0 for the color stops.</param>
26+
/// <param name="referenceAxisEnd">The end point of the reference axis of the ellipse.</param>
27+
/// <param name="axisRatio">
28+
/// The ratio of the axis widths.
29+
/// The second axis' is perpendicular to the reference axis and
30+
/// it's length is the reference axis' length multiplied by this factor.
31+
/// </param>
32+
/// <param name="repetitionMode">Defines how the colors of the gradients are repeated.</param>
33+
/// <param name="colorStops">the color stops as defined in base class.</param>
34+
public EllipticGradientBrush(
35+
Point center,
36+
Point referenceAxisEnd,
37+
float axisRatio,
38+
GradientRepetitionMode repetitionMode,
39+
params ColorStop<TPixel>[] colorStops)
40+
: base(repetitionMode, colorStops)
41+
{
42+
this.center = center;
43+
this.referenceAxisEnd = referenceAxisEnd;
44+
this.axisRatio = axisRatio;
45+
}
46+
47+
/// <inheritdoc cref="CreateApplicator" />
48+
public override BrushApplicator<TPixel> CreateApplicator(
49+
ImageFrame<TPixel> source,
50+
RectangleF region,
51+
GraphicsOptions options) =>
52+
new RadialGradientBrushApplicator(
53+
source,
54+
options,
55+
this.center,
56+
this.referenceAxisEnd,
57+
this.axisRatio,
58+
this.ColorStops,
59+
this.RepetitionMode);
60+
61+
/// <inheritdoc />
62+
private sealed class RadialGradientBrushApplicator : GradientBrushApplicatorBase
63+
{
64+
private readonly Point center;
65+
66+
private readonly Point referenceAxisEnd;
67+
68+
private readonly float axisRatio;
69+
70+
private readonly double rotation;
71+
72+
private readonly float referenceRadius;
73+
74+
private readonly float secondRadius;
75+
76+
private readonly float cosRotation;
77+
78+
private readonly float sinRotation;
79+
80+
private readonly float secondRadiusSquared;
81+
82+
private readonly float referenceRadiusSquared;
83+
84+
/// <summary>
85+
/// Initializes a new instance of the <see cref="RadialGradientBrushApplicator" /> class.
86+
/// </summary>
87+
/// <param name="target">The target image</param>
88+
/// <param name="options">The options</param>
89+
/// <param name="center">Center of the ellipse</param>
90+
/// <param name="referenceAxisEnd">Point on one angular points of the ellipse.</param>
91+
/// <param name="axisRatio">
92+
/// Ratio of the axis length's. Used to determine the length of the second axis,
93+
/// the first is defined by <see cref="center"/> and <see cref="referenceAxisEnd"/>.</param>
94+
/// <param name="colorStops">Definition of colors</param>
95+
/// <param name="repetitionMode">Defines how the gradient colors are repeated.</param>
96+
public RadialGradientBrushApplicator(
97+
ImageFrame<TPixel> target,
98+
GraphicsOptions options,
99+
Point center,
100+
Point referenceAxisEnd,
101+
float axisRatio,
102+
ColorStop<TPixel>[] colorStops,
103+
GradientRepetitionMode repetitionMode)
104+
: base(target, options, colorStops, repetitionMode)
105+
{
106+
this.center = center;
107+
this.referenceAxisEnd = referenceAxisEnd;
108+
this.axisRatio = axisRatio;
109+
this.rotation = this.AngleBetween(
110+
this.center,
111+
new PointF(this.center.X + 1, this.center.Y),
112+
this.referenceAxisEnd);
113+
this.referenceRadius = this.DistanceBetween(this.center, this.referenceAxisEnd);
114+
this.secondRadius = this.referenceRadius * this.axisRatio;
115+
116+
this.referenceRadiusSquared = this.referenceRadius * this.referenceRadius;
117+
this.secondRadiusSquared = this.secondRadius * this.secondRadius;
118+
119+
this.sinRotation = (float)Math.Sin(this.rotation);
120+
this.cosRotation = (float)Math.Cos(this.rotation);
121+
}
122+
123+
/// <inheritdoc />
124+
public override void Dispose()
125+
{
126+
}
127+
128+
/// <inheritdoc />
129+
protected override float PositionOnGradient(int xt, int yt)
130+
{
131+
float x0 = xt - this.center.X;
132+
float y0 = yt - this.center.Y;
133+
134+
float x = (x0 * this.cosRotation) - (y0 * this.sinRotation);
135+
float y = (x0 * this.sinRotation) + (y0 * this.cosRotation);
136+
137+
float xSquared = x * x;
138+
float ySquared = y * y;
139+
140+
var inBoundaryChecker = (xSquared / this.referenceRadiusSquared)
141+
+ (ySquared / this.secondRadiusSquared);
142+
143+
return inBoundaryChecker;
144+
}
145+
146+
private float AngleBetween(PointF junction, PointF a, PointF b)
147+
{
148+
var vA = a - junction;
149+
var vB = b - junction;
150+
return (float)(Math.Atan2(vB.Y, vB.X)
151+
- Math.Atan2(vA.Y, vA.X));
152+
}
153+
154+
private float DistanceBetween(
155+
PointF p1,
156+
PointF p2)
157+
{
158+
float dX = p1.X - p2.X;
159+
float dXsquared = dX * dX;
160+
161+
float dY = p1.Y - p2.Y;
162+
float dYsquared = dY * dY;
163+
return (float)Math.Sqrt(dXsquared + dYsquared);
164+
}
165+
}
166+
}
167+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
using System;
2+
using System.Numerics;
3+
4+
using SixLabors.ImageSharp.PixelFormats;
5+
using SixLabors.ImageSharp.PixelFormats.PixelBlenders;
6+
using SixLabors.Primitives;
7+
8+
namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes
9+
{
10+
/// <summary>
11+
/// Base class for Gradient brushes
12+
/// </summary>
13+
/// <typeparam name="TPixel">The pixel format</typeparam>
14+
public abstract class GradientBrushBase<TPixel> : IBrush<TPixel>
15+
where TPixel : struct, IPixel<TPixel>
16+
{
17+
/// <inheritdoc cref="IBrush{TPixel}"/>
18+
/// <param name="repetitionMode">Defines how the colors are repeated beyond the interval [0..1]</param>
19+
/// <param name="colorStops">The gradient colors.</param>
20+
protected GradientBrushBase(
21+
GradientRepetitionMode repetitionMode,
22+
params ColorStop<TPixel>[] colorStops)
23+
{
24+
this.RepetitionMode = repetitionMode;
25+
this.ColorStops = colorStops;
26+
}
27+
28+
/// <summary>
29+
/// Gets how the colors are repeated beyond the interval [0..1].
30+
/// </summary>
31+
protected GradientRepetitionMode RepetitionMode { get; }
32+
33+
/// <summary>
34+
/// Gets the list of color stops for this gradient.
35+
/// </summary>
36+
protected ColorStop<TPixel>[] ColorStops { get; }
37+
38+
/// <inheritdoc cref="IBrush{TPixel}" />
39+
public abstract BrushApplicator<TPixel> CreateApplicator(
40+
ImageFrame<TPixel> source,
41+
RectangleF region,
42+
GraphicsOptions options);
43+
44+
/// <summary>
45+
/// Base class for gradient brush applicators
46+
/// </summary>
47+
protected abstract class GradientBrushApplicatorBase : BrushApplicator<TPixel>
48+
{
49+
private readonly ColorStop<TPixel>[] colorStops;
50+
51+
private readonly GradientRepetitionMode repetitionMode;
52+
53+
/// <summary>
54+
/// Initializes a new instance of the <see cref="GradientBrushApplicatorBase"/> class.
55+
/// </summary>
56+
/// <param name="target">The target.</param>
57+
/// <param name="options">The options.</param>
58+
/// <param name="colorStops">An array of color stops sorted by their position.</param>
59+
/// <param name="repetitionMode">Defines if and how the gradient should be repeated.</param>
60+
protected GradientBrushApplicatorBase(
61+
ImageFrame<TPixel> target,
62+
GraphicsOptions options,
63+
ColorStop<TPixel>[] colorStops,
64+
GradientRepetitionMode repetitionMode)
65+
: base(target, options)
66+
{
67+
this.colorStops = colorStops; // TODO: requires colorStops to be sorted by position - should that be checked?
68+
this.repetitionMode = repetitionMode;
69+
}
70+
71+
/// <summary>
72+
/// Base implementation of the indexer for gradients
73+
/// (follows the facade pattern, using abstract methods)
74+
/// </summary>
75+
/// <param name="x">X coordinate of the Pixel.</param>
76+
/// <param name="y">Y coordinate of the Pixel.</param>
77+
internal override TPixel this[int x, int y]
78+
{
79+
get
80+
{
81+
float positionOnCompleteGradient = this.PositionOnGradient(x, y);
82+
83+
switch (this.repetitionMode)
84+
{
85+
case GradientRepetitionMode.None:
86+
// do nothing. The following could be done, but is not necessary:
87+
// onLocalGradient = Math.Min(0, Math.Max(1, onLocalGradient));
88+
break;
89+
case GradientRepetitionMode.Repeat:
90+
positionOnCompleteGradient = positionOnCompleteGradient % 1;
91+
break;
92+
case GradientRepetitionMode.Reflect:
93+
positionOnCompleteGradient = positionOnCompleteGradient % 2;
94+
if (positionOnCompleteGradient > 1)
95+
{
96+
positionOnCompleteGradient = 2 - positionOnCompleteGradient;
97+
}
98+
99+
break;
100+
case GradientRepetitionMode.DontFill:
101+
if (positionOnCompleteGradient > 1 || positionOnCompleteGradient < 0)
102+
{
103+
return NamedColors<TPixel>.Transparent;
104+
}
105+
106+
break;
107+
default:
108+
throw new ArgumentOutOfRangeException();
109+
}
110+
111+
var (from, to) = this.GetGradientSegment(positionOnCompleteGradient);
112+
113+
if (from.Color.Equals(to.Color))
114+
{
115+
return from.Color;
116+
}
117+
else
118+
{
119+
var fromAsVector = from.Color.ToVector4();
120+
var toAsVector = to.Color.ToVector4();
121+
float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / to.Ratio;
122+
123+
// TODO: this should be changeble for different gradienting functions
124+
Vector4 result = PorterDuffFunctions.Normal(
125+
fromAsVector,
126+
toAsVector,
127+
onLocalGradient);
128+
129+
TPixel resultColor = default;
130+
resultColor.PackFromVector4(result);
131+
return resultColor;
132+
}
133+
}
134+
}
135+
136+
/// <summary>
137+
/// calculates the position on the gradient for a given pixel.
138+
/// This method is abstract as it's content depends on the shape of the gradient.
139+
/// </summary>
140+
/// <param name="x">The x coordinate of the pixel</param>
141+
/// <param name="y">The y coordinate of the pixel</param>
142+
/// <returns>
143+
/// The position the given pixel has on the gradient.
144+
/// The position is not bound to the [0..1] interval.
145+
/// Values outside of that interval may be treated differently,
146+
/// e.g. for the <see cref="GradientRepetitionMode" /> enum.
147+
/// </returns>
148+
protected abstract float PositionOnGradient(int x, int y);
149+
150+
private (ColorStop<TPixel> from, ColorStop<TPixel> to) GetGradientSegment(
151+
float positionOnCompleteGradient)
152+
{
153+
var localGradientFrom = this.colorStops[0];
154+
ColorStop<TPixel> localGradientTo = default;
155+
156+
// TODO: ensure colorStops has at least 2 items (technically 1 would be okay, but that's no gradient)
157+
foreach (var colorStop in this.colorStops)
158+
{
159+
localGradientTo = colorStop;
160+
161+
if (colorStop.Ratio > positionOnCompleteGradient)
162+
{
163+
// we're done here, so break it!
164+
break;
165+
}
166+
167+
localGradientFrom = localGradientTo;
168+
}
169+
170+
return (localGradientFrom, localGradientTo);
171+
}
172+
}
173+
}
174+
}

0 commit comments

Comments
 (0)