Skip to content

Commit d284b76

Browse files
committed
#86: started to work on Gradient Brushes: Linear gradient brush,
not yet working - what's wrong?
1 parent 07a811c commit d284b76

File tree

2 files changed

+258
-0
lines changed

2 files changed

+258
-0
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
using System;
2+
using System.Numerics;
3+
4+
using SixLabors.ImageSharp.Advanced;
5+
using SixLabors.ImageSharp.Memory;
6+
using SixLabors.ImageSharp.PixelFormats;
7+
using SixLabors.ImageSharp.PixelFormats.PixelBlenders;
8+
using SixLabors.Primitives;
9+
10+
namespace SixLabors.ImageSharp.Processing.Drawing.Brushes
11+
{
12+
/// <summary>
13+
/// Provides an implementation of a brush for painting gradients within areas.
14+
/// Supported right now:
15+
/// - a set of colors in relative distances to each other.
16+
/// - two points to gradient along.
17+
/// </summary>
18+
/// <typeparam name="TPixel">The pixel format</typeparam>
19+
public class LinearGradientBrush<TPixel> : IBrush<TPixel>
20+
where TPixel : struct, IPixel<TPixel>
21+
{
22+
private readonly Point p1;
23+
24+
private readonly Point p2;
25+
26+
private readonly Tuple<float, TPixel>[] keyColors;
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="LinearGradientBrush{TPixel}"/> class.
30+
/// </summary>
31+
/// <param name="p1">Start point</param>
32+
/// <param name="p2">End point</param>
33+
/// <param name="keyColors">a set of color keys and where they are. The double must be in range [0..1] and is relative between p1 and p2.</param>
34+
public LinearGradientBrush(Point p1, Point p2, params Tuple<float, TPixel>[] keyColors)
35+
{
36+
this.p1 = p1;
37+
this.p2 = p2;
38+
this.keyColors = keyColors;
39+
}
40+
41+
/// <inheritdoc />
42+
public BrushApplicator<TPixel> CreateApplicator(ImageFrame<TPixel> source, RectangleF region, GraphicsOptions options)
43+
=> new LinearGradientBrushApplicator(source, this.p1, this.p2, this.keyColors, region, options);
44+
45+
/// <summary>
46+
/// The linear gradient brush applicator.
47+
/// </summary>
48+
private class LinearGradientBrushApplicator : BrushApplicator<TPixel>
49+
{
50+
private readonly Point start;
51+
52+
private readonly Point end;
53+
54+
private readonly Tuple<float, TPixel>[] colorStops;
55+
56+
/// <summary>
57+
/// the vector along the gradient, x component
58+
/// </summary>
59+
private readonly float alongX;
60+
61+
/// <summary>
62+
/// the vector along the gradient, y component
63+
/// </summary>
64+
private readonly float alongY;
65+
66+
/// <summary>
67+
/// the vector perpendicular to the gradient, y component
68+
/// </summary>
69+
private readonly float acrossY;
70+
71+
/// <summary>
72+
/// the vector perpendicular to the gradient, x component
73+
/// </summary>
74+
private readonly float acrossX;
75+
76+
/// <summary>
77+
/// helper to speed up calculation as these dont't change
78+
/// </summary>
79+
private readonly float aYcX;
80+
81+
/// <summary>
82+
/// helper to speed up calculation as these dont't change
83+
/// </summary>
84+
private readonly float aXcY;
85+
86+
/// <summary>
87+
/// helper to speed up calculation as these dont't change
88+
/// </summary>
89+
private readonly float aXcX;
90+
91+
/// <summary>
92+
/// Initializes a new instance of the <see cref="LinearGradientBrushApplicator" /> class.
93+
/// </summary>
94+
/// <param name="source"></param>
95+
/// <param name="start">start point of the gradient</param>
96+
/// <param name="end">end point of the gradient</param>
97+
/// <param name="colorStops">tuple list of colors and their respective position between 0 and 1 on the line</param>
98+
/// <param name="region"></param>
99+
/// <param name="options">the graphics options</param>
100+
public LinearGradientBrushApplicator(
101+
ImageFrame<TPixel> source,
102+
Point start,
103+
Point end,
104+
Tuple<float, TPixel>[] colorStops,
105+
RectangleF region, // TODO: use region, compare with other Brushes for reference.
106+
GraphicsOptions options)
107+
: base(source, options)
108+
{
109+
this.start = start;
110+
this.end = end;
111+
this.colorStops = colorStops; // TODO: requires colorStops to be sorted by Item1!
112+
113+
// the along vector:
114+
this.alongX = this.start.X - this.end.X;
115+
this.alongY = this.start.Y - this.end.Y;
116+
117+
// the cross vector:
118+
this.acrossX = this.alongY;
119+
this.acrossY = -this.alongX;
120+
121+
// some helpers:
122+
this.aYcX = this.alongY * this.acrossX;
123+
this.aXcY = this.alongX * this.acrossY;
124+
this.aXcX = this.alongX * this.acrossX;
125+
}
126+
127+
/// <summary>
128+
/// Gets the color for a single pixel
129+
/// </summary>
130+
/// <param name="x">The x.</param>
131+
/// <param name="y">The y.</param>
132+
internal override TPixel this[int x, int y]
133+
{
134+
get
135+
{
136+
// the following formula is the result of the linear equation system that forms the vector.
137+
// TODO: this formula should be abstracted as it's the only difference between linear and radial gradient!
138+
float onCompleteGradient = this.RatioOnGradient(x, y);
139+
140+
var localGradientFrom = this.colorStops[0];
141+
Tuple<float, TPixel> localGradientTo = null;
142+
143+
// TODO: ensure colorStops has at least 2 items (technically 1 would be okay, but that's no gradient)
144+
foreach (var colorStop in this.colorStops)
145+
{
146+
localGradientTo = colorStop;
147+
if (colorStop.Item1 >= onCompleteGradient)
148+
{
149+
// we're done here, so break it!
150+
break;
151+
}
152+
153+
localGradientFrom = localGradientTo;
154+
}
155+
156+
TPixel resultColor = default;
157+
if (localGradientFrom.Item2.Equals(localGradientTo.Item2))
158+
{
159+
resultColor = localGradientFrom.Item2;
160+
}
161+
else
162+
{
163+
var fromAsVector = localGradientFrom.Item2.ToVector4();
164+
var toAsVector = localGradientTo.Item2.ToVector4();
165+
float onLocalGradient = (onCompleteGradient - localGradientFrom.Item1) / localGradientTo.Item1; // TODO:
166+
167+
Vector4 result = PorterDuffFunctions.Normal(
168+
fromAsVector,
169+
toAsVector,
170+
onLocalGradient);
171+
172+
// TODO: when resultColor is a struct, what does PackFromVector4 do here?
173+
resultColor.PackFromVector4(result);
174+
}
175+
176+
return resultColor;
177+
}
178+
}
179+
180+
private float RatioOnGradient(int x, int y)
181+
{
182+
return ((x / this.acrossX) - (this.alongX * y / this.aYcX))
183+
/ (1 - (this.aXcY / this.aXcX));
184+
}
185+
186+
internal override void Apply(Span<float> scanline, int x, int y)
187+
{
188+
base.Apply(scanline, x, y);
189+
190+
// Span<TPixel> destinationRow = this.Target.GetPixelRowSpan(y).Slice(x, scanline.Length);
191+
// MemoryManager memoryManager = this.Target.MemoryManager;
192+
// using (IBuffer<float> amountBuffer = memoryManager.Allocate<float>(scanline.Length))
193+
// {
194+
// Span<float> amountSpan = amountBuffer.Span;
195+
//
196+
// for (int i = 0; i < scanline.Length; i++)
197+
// {
198+
// amountSpan[i] = scanline[i] * this.Options.BlendPercentage;
199+
// }
200+
//
201+
// this.Blender.Blend(memoryManager, destinationRow, destinationRow, this.Colors.Span, amountSpan);
202+
// }
203+
}
204+
205+
/// <inheritdoc />
206+
public override void Dispose()
207+
{
208+
}
209+
}
210+
}
211+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) Six Labors and contributors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System.Numerics;
5+
using SixLabors.ImageSharp.PixelFormats;
6+
using SixLabors.ImageSharp.Processing.Drawing;
7+
using Xunit;
8+
9+
namespace SixLabors.ImageSharp.Tests.Drawing
10+
{
11+
using System;
12+
13+
using SixLabors.ImageSharp.Processing;
14+
using SixLabors.ImageSharp.Processing.Drawing.Brushes;
15+
using SixLabors.ImageSharp.Processing.Overlays;
16+
17+
using Point = SixLabors.Primitives.Point;
18+
19+
public class FillLinearGradientBrushTests : FileTestBase
20+
{
21+
[Fact]
22+
public void LinearGradientBrushWithEqualColorsReturnsUnicolorImage()
23+
{
24+
string path = TestEnvironment.CreateOutputDirectory("Fill", "LinearGradientBrush");
25+
using (var image = new Image<Rgba32>(500, 500))
26+
{
27+
LinearGradientBrush<Rgba32> unicolorLinearGradientBrush =
28+
new LinearGradientBrush<Rgba32>(
29+
new Point(0, 0),
30+
new Point(500, 0),
31+
new Tuple<float, Rgba32>(0, Rgba32.Red),
32+
new Tuple<float, Rgba32>(1, Rgba32.Red));
33+
34+
image.Mutate(x => x.Fill(unicolorLinearGradientBrush));
35+
image.Save($"{path}/UnicolorGradient.png");
36+
37+
using (PixelAccessor<Rgba32> sourcePixels = image.Lock())
38+
{
39+
Assert.Equal(Rgba32.Red, sourcePixels[0, 0]);
40+
Assert.Equal(Rgba32.Red, sourcePixels[9, 9]);
41+
Assert.Equal(Rgba32.Red, sourcePixels[199, 149]);
42+
Assert.Equal(Rgba32.Red, sourcePixels[500, 500]);
43+
}
44+
}
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)