diff --git a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/AbstractGradientBrush{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/AbstractGradientBrush{TPixel}.cs new file mode 100644 index 000000000..2939aed7d --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/AbstractGradientBrush{TPixel}.cs @@ -0,0 +1,132 @@ +using System.Numerics; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.PixelFormats.PixelBlenders; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes +{ + /// + /// Base class for Gradient brushes + /// + /// The pixel format + public abstract class AbstractGradientBrush : IBrush + where TPixel : struct, IPixel + { + /// + /// The gradient colors. + protected AbstractGradientBrush(params ColorStop[] colorStops) + { + this.ColorStops = colorStops; + } + + /// + /// Gets the list of color stops for this gradient. + /// + protected ColorStop[] ColorStops { get; } + + /// + public abstract BrushApplicator CreateApplicator( + ImageFrame source, + RectangleF region, + GraphicsOptions options); + + /// + /// Base class for gradient brush applicators + /// + protected abstract class AbstractGradientBrushApplicator : BrushApplicator + { + private readonly ColorStop[] colorStops; + + /// + /// Initializes a new instance of the class. + /// + /// The target. + /// The options. + /// an array of color stops sorted by their position. + /// TODO: use region, compare with other Brushes for reference + protected AbstractGradientBrushApplicator( + ImageFrame target, + GraphicsOptions options, + ColorStop[] colorStops, + RectangleF region) + : base(target, options) + { + this.colorStops = colorStops; // TODO: requires colorStops to be sorted by position - should that be checked? + } + + /// + /// Base implementation of the indexer for gradients + /// (follows the facade pattern, using abstract methods) + /// + /// X coordinate of the Pixel. + /// Y coordinate of the Pixel. + internal override TPixel this[int x, int y] + { + get + { + float positionOnCompleteGradient = this.PositionOnGradient(x, y); + var (from, to) = this.GetGradientSegment(positionOnCompleteGradient); + + if (from.Color.Equals(to.Color)) + { + return from.Color; + } + else + { + var fromAsVector = from.Color.ToVector4(); + var toAsVector = to.Color.ToVector4(); + float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / to.Ratio; + + // TODO: this should be changeble for different gradienting functions + Vector4 result = PorterDuffFunctions.Normal( + fromAsVector, + toAsVector, + onLocalGradient); + + TPixel resultColor = default; + resultColor.PackFromVector4(result); + return resultColor; + } + } + } + + /// + /// calculates the position on the gradient for a given pixel. + /// This method is abstract as it's content depends on the shape of the gradient. + /// + /// The x coordinate of the pixel + /// The y coordinate of the pixel + /// + /// The position the given pixel has on the gradient. + /// The position is not bound to the [0..1] interval. + /// Values outside of that interval may be treated differently, + /// e.g. for the enum. + /// + protected abstract float PositionOnGradient(int x, int y); + + private (ColorStop from, ColorStop to) GetGradientSegment( + float positionOnCompleteGradient) + { + var localGradientFrom = this.colorStops[0]; + ColorStop localGradientTo = default; + + // TODO: ensure colorStops has at least 2 items (technically 1 would be okay, but that's no gradient) + foreach (var colorStop in this.colorStops) + { + localGradientTo = colorStop; + + if (colorStop.Ratio > positionOnCompleteGradient) + { + // we're done here, so break it! + break; + } + + localGradientFrom = localGradientTo; + } + + return (localGradientFrom, localGradientTo); + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/ColorStop.cs b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/ColorStop.cs new file mode 100644 index 000000000..298af5cb5 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/ColorStop.cs @@ -0,0 +1,36 @@ +using System.Diagnostics; + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes +{ + /// + /// A struct that defines a single color stop. + /// + /// The pixel format. + [DebuggerDisplay("ColorStop({Ratio} -> {Color}")] + public struct ColorStop + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the struct. + /// + /// Where should it be? 0 is at the start, 1 at the end of the Gradient. + /// What color should be used at that point? + public ColorStop(float ratio, TPixel color) + { + this.Ratio = ratio; + this.Color = color; + } + + /// + /// Gets the point along the defined gradient axis. + /// + public float Ratio { get; } + + /// + /// Gets the color to be used. + /// + public TPixel Color { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/LinearGradientBrush{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/LinearGradientBrush{TPixel}.cs similarity index 54% rename from src/ImageSharp.Drawing/Processing/Drawing/Brushes/LinearGradientBrush{TPixel}.cs rename to src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/LinearGradientBrush{TPixel}.cs index c00ecb193..ba398995b 100644 --- a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/LinearGradientBrush{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/LinearGradientBrush{TPixel}.cs @@ -1,89 +1,49 @@ using System; -using System.Diagnostics; -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.PixelFormats.PixelBlenders; using SixLabors.Primitives; -namespace SixLabors.ImageSharp.Processing.Drawing.Brushes +namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes { /// - /// Provides an implementation of a brush for painting gradients within areas. + /// Provides an implementation of a brush for painting linear gradients within areas. /// Supported right now: /// - a set of colors in relative distances to each other. - /// - two points to gradient along. /// /// The pixel format - public class LinearGradientBrush : IBrush + public class LinearGradientBrush : AbstractGradientBrush where TPixel : struct, IPixel { private readonly Point p1; private readonly Point p2; - private readonly ColorStop[] colorStops; - /// /// Initializes a new instance of the class. /// /// Start point /// End point - /// - /// A set of color keys and where they are. - /// The double should be in range [0..1] and is relative between p1 and p2. - /// TODO: what about the [0..1] restriction? is it necessary? If so, it should be checked, if not, it should be explained what happens for greater/smaller values. - /// - public LinearGradientBrush(Point p1, Point p2, params ColorStop[] colorStops) + /// + public LinearGradientBrush(Point p1, Point p2, params ColorStop[] colorStops) + : base(colorStops) { this.p1 = p1; this.p2 = p2; - this.colorStops = colorStops; } /// - public BrushApplicator CreateApplicator(ImageFrame source, RectangleF region, GraphicsOptions options) - => new LinearGradientBrushApplicator(source, this.p1, this.p2, this.colorStops, region, options); - - /// - /// A struct that defines a single color stop. - /// - [DebuggerDisplay("ColorStop({Ratio} -> {Color}")] - public struct ColorStop - { - /// - /// Initializes a new instance of the struct. - /// - /// Where should it be? 0 is at the start, 1 at the end of the . - /// What color should be used at that point? - public ColorStop(float ratio, TPixel color) - { - this.Ratio = ratio; - this.Color = color; - } - - /// - /// Gets the point along the defined gradient axis. - /// - public float Ratio { get; } - - /// - /// Gets the color to be used. - /// - public TPixel Color { get; } - } + public override BrushApplicator CreateApplicator(ImageFrame source, RectangleF region, GraphicsOptions options) + => new LinearGradientBrushApplicator(source, this.p1, this.p2, this.ColorStops, region, options); /// /// The linear gradient brush applicator. /// - private class LinearGradientBrushApplicator : BrushApplicator + private class LinearGradientBrushApplicator : AbstractGradientBrushApplicator { private readonly Point start; private readonly Point end; - private readonly ColorStop[] colorStops; - /// /// the vector along the gradient, x component /// @@ -127,14 +87,13 @@ namespace SixLabors.ImageSharp.Processing.Drawing.Brushes ImageFrame source, Point start, Point end, - ColorStop[] colorStops, - RectangleF region, // TODO: use region, compare with other Brushes for reference. + ColorStop[] colorStops, + RectangleF region, GraphicsOptions options) - : base(source, options) + : base(source, options, colorStops, region) { this.start = start; this.end = end; - this.colorStops = colorStops; // TODO: requires colorStops to be sorted by Item1! // the along vector: this.alongX = this.end.X - this.start.X; @@ -149,61 +108,7 @@ namespace SixLabors.ImageSharp.Processing.Drawing.Brushes this.length = (float)Math.Sqrt(this.alongsSquared); } - /// - /// Gets the color for a single pixel - /// - /// The x coordinate. - /// The y coordinate. - internal override TPixel this[int x, int y] - { - get - { - // the following formula is the result of the linear equation system that forms the vector. - // TODO: this formula should be abstracted as it's the only difference between linear and radial gradient! - float onCompleteGradient = this.RatioOnGradient(x, y); - - var localGradientFrom = this.colorStops[0]; - ColorStop localGradientTo = default; - - // TODO: ensure colorStops has at least 2 items (technically 1 would be okay, but that's no gradient) - foreach (var colorStop in this.colorStops) - { - localGradientTo = colorStop; - - if (colorStop.Ratio > onCompleteGradient) - { - // we're done here, so break it! - break; - } - - localGradientFrom = localGradientTo; - } - - TPixel resultColor = default; - if (localGradientFrom.Color.Equals(localGradientTo.Color)) - { - resultColor = localGradientFrom.Color; - } - else - { - var fromAsVector = localGradientFrom.Color.ToVector4(); - var toAsVector = localGradientTo.Color.ToVector4(); - float onLocalGradient = (onCompleteGradient - localGradientFrom.Ratio) / localGradientTo.Ratio; // TODO: - - Vector4 result = PorterDuffFunctions.Normal( - fromAsVector, - toAsVector, - onLocalGradient); - - // TODO: when resultColor is a struct, what does PackFromVector4 do here? - resultColor.PackFromVector4(result); - } - - return resultColor; - } - } - - private float RatioOnGradient(int x, int y) + protected override float PositionOnGradient(int x, int y) { if (this.acrossX == 0) { @@ -234,6 +139,10 @@ namespace SixLabors.ImageSharp.Processing.Drawing.Brushes } } + public override void Dispose() + { + } + internal override void Apply(Span scanline, int x, int y) { base.Apply(scanline, x, y); @@ -257,11 +166,6 @@ namespace SixLabors.ImageSharp.Processing.Drawing.Brushes // this.Blender.Blend(memoryManager, destinationRow, destinationRow, this.Colors.Span, amountSpan); // } } - - /// - public override void Dispose() - { - } } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs b/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs index c95b165f8..b9d37d8e8 100644 --- a/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs +++ b/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs @@ -2,11 +2,12 @@ // Licensed under the Apache License, Version 2.0. using System.Linq; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Drawing.Brushes; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Drawing; +using SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes; + using Xunit; namespace SixLabors.ImageSharp.Tests.Drawing @@ -23,8 +24,8 @@ namespace SixLabors.ImageSharp.Tests.Drawing new LinearGradientBrush( new SixLabors.Primitives.Point(0, 0), new SixLabors.Primitives.Point(10, 0), - new LinearGradientBrush.ColorStop(0, Rgba32.Red), - new LinearGradientBrush.ColorStop(1, Rgba32.Red)); + new ColorStop(0, Rgba32.Red), + new ColorStop(1, Rgba32.Red)); image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); image.Save($"{path}/UnicolorGradient.png"); @@ -53,8 +54,8 @@ namespace SixLabors.ImageSharp.Tests.Drawing new LinearGradientBrush( new SixLabors.Primitives.Point(0, 0), new SixLabors.Primitives.Point(500, 0), - new LinearGradientBrush.ColorStop(0, Rgba32.Red), - new LinearGradientBrush.ColorStop(1, Rgba32.Yellow)); + new ColorStop(0, Rgba32.Red), + new ColorStop(1, Rgba32.Yellow)); image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); image.Save($"{path}/horizontalRedToYellow.png"); @@ -97,16 +98,16 @@ namespace SixLabors.ImageSharp.Tests.Drawing Assert.True(pattern.Length > 0); // create the input pattern: 0, followed by each of the arguments twice, followed by 1.0 - toggling black and white. - LinearGradientBrush.ColorStop[] colorStops = - Enumerable.Repeat(new LinearGradientBrush.ColorStop(0, Rgba32.Black), 1) + ColorStop[] colorStops = + Enumerable.Repeat(new ColorStop(0, Rgba32.Black), 1) .Concat( pattern .SelectMany((f, index) => new[] { - new LinearGradientBrush.ColorStop(f, index % 2 == 0 ? Rgba32.Black : Rgba32.White), - new LinearGradientBrush.ColorStop(f, index % 2 == 0 ? Rgba32.White : Rgba32.Black) + new ColorStop(f, index % 2 == 0 ? Rgba32.Black : Rgba32.White), + new ColorStop(f, index % 2 == 0 ? Rgba32.White : Rgba32.Black) })) - .Concat(Enumerable.Repeat(new LinearGradientBrush.ColorStop(1, pattern.Length % 2 == 0 ? Rgba32.Black : Rgba32.White), 1)) + .Concat(Enumerable.Repeat(new ColorStop(1, pattern.Length % 2 == 0 ? Rgba32.Black : Rgba32.White), 1)) .ToArray(); string path = TestEnvironment.CreateOutputDirectory("Fill", "LinearGradientBrush"); @@ -145,8 +146,8 @@ namespace SixLabors.ImageSharp.Tests.Drawing new LinearGradientBrush( new SixLabors.Primitives.Point(0, 0), new SixLabors.Primitives.Point(0, 500), - new LinearGradientBrush.ColorStop(0, Rgba32.Red), - new LinearGradientBrush.ColorStop(1, Rgba32.Yellow)); + new ColorStop(0, Rgba32.Red), + new ColorStop(1, Rgba32.Yellow)); image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); image.Save($"{path}/verticalRedToYellow.png"); @@ -154,7 +155,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing using (PixelAccessor sourcePixels = image.Lock()) { Rgba32 firstRowColor = sourcePixels[0, 0]; - + Rgba32 columnColor23 = sourcePixels[0, 23]; Rgba32 columnColor42 = sourcePixels[0, 42]; Rgba32 columnColor333 = sourcePixels[0, 333]; @@ -193,8 +194,8 @@ namespace SixLabors.ImageSharp.Tests.Drawing new LinearGradientBrush( new SixLabors.Primitives.Point(startX, startY), new SixLabors.Primitives.Point(endX, endY), - new LinearGradientBrush.ColorStop(0, Rgba32.Red), - new LinearGradientBrush.ColorStop(1, Rgba32.Yellow)); + new ColorStop(0, Rgba32.Red), + new ColorStop(1, Rgba32.Yellow)); image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); image.Save($"{path}/diagonalRedToYellowFrom{startX}_{startY}.png"); @@ -233,10 +234,10 @@ namespace SixLabors.ImageSharp.Tests.Drawing Rgba32.Red }; - var colorStops = new LinearGradientBrush.ColorStop[stopPositions.Length]; + var colorStops = new ColorStop[stopPositions.Length]; for (int i = 0; i < stopPositions.Length; i++) { - colorStops[i] = new LinearGradientBrush.ColorStop( + colorStops[i] = new ColorStop( stopPositions[i], colors[stopColorCodes[i]]); }