diff --git a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/LinearGradientBrush.cs b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/LinearGradientBrush.cs new file mode 100644 index 0000000000..bfbeded698 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/LinearGradientBrush.cs @@ -0,0 +1,211 @@ +using System; +using System.Numerics; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.PixelFormats.PixelBlenders; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Drawing.Brushes +{ + /// + /// Provides an implementation of a brush for painting 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 + where TPixel : struct, IPixel + { + private readonly Point p1; + + private readonly Point p2; + + private readonly Tuple[] keyColors; + + /// + /// Initializes a new instance of the class. + /// + /// Start point + /// End point + /// a set of color keys and where they are. The double must be in range [0..1] and is relative between p1 and p2. + public LinearGradientBrush(Point p1, Point p2, params Tuple[] keyColors) + { + this.p1 = p1; + this.p2 = p2; + this.keyColors = keyColors; + } + + /// + public BrushApplicator CreateApplicator(ImageFrame source, RectangleF region, GraphicsOptions options) + => new LinearGradientBrushApplicator(source, this.p1, this.p2, this.keyColors, region, options); + + /// + /// The linear gradient brush applicator. + /// + private class LinearGradientBrushApplicator : BrushApplicator + { + private readonly Point start; + + private readonly Point end; + + private readonly Tuple[] colorStops; + + /// + /// the vector along the gradient, x component + /// + private readonly float alongX; + + /// + /// the vector along the gradient, y component + /// + private readonly float alongY; + + /// + /// the vector perpendicular to the gradient, y component + /// + private readonly float acrossY; + + /// + /// the vector perpendicular to the gradient, x component + /// + private readonly float acrossX; + + /// + /// helper to speed up calculation as these dont't change + /// + private readonly float aYcX; + + /// + /// helper to speed up calculation as these dont't change + /// + private readonly float aXcY; + + /// + /// helper to speed up calculation as these dont't change + /// + private readonly float aXcX; + + /// + /// Initializes a new instance of the class. + /// + /// The source + /// start point of the gradient + /// end point of the gradient + /// tuple list of colors and their respective position between 0 and 1 on the line + /// the region, copied from SolidColorBrush, not sure if necessary! TODO + /// the graphics options + public LinearGradientBrushApplicator( + ImageFrame source, + Point start, + Point end, + Tuple[] colorStops, + RectangleF region, // TODO: use region, compare with other Brushes for reference. + GraphicsOptions options) + : base(source, options) + { + this.start = start; + this.end = end; + this.colorStops = colorStops; // TODO: requires colorStops to be sorted by Item1! + + // the along vector: + this.alongX = this.start.X - this.end.X; + this.alongY = this.start.Y - this.end.Y; + + // the cross vector: + this.acrossX = this.alongY; + this.acrossY = -this.alongX; + + // some helpers: + this.aYcX = this.alongY * this.acrossX; + this.aXcY = this.alongX * this.acrossY; + this.aXcX = this.alongX * this.acrossX; + } + + /// + /// Gets the color for a single pixel + /// + /// The x. + /// The y. + 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]; + Tuple localGradientTo = null; + + // 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.Item1 >= onCompleteGradient) + { + // we're done here, so break it! + break; + } + + localGradientFrom = localGradientTo; + } + + TPixel resultColor = default; + if (localGradientFrom.Item2.Equals(localGradientTo.Item2)) + { + resultColor = localGradientFrom.Item2; + } + else + { + var fromAsVector = localGradientFrom.Item2.ToVector4(); + var toAsVector = localGradientTo.Item2.ToVector4(); + float onLocalGradient = (onCompleteGradient - localGradientFrom.Item1) / localGradientTo.Item1; // 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) + { + return ((x / this.acrossX) - (this.alongX * y / this.aYcX)) + / (1 - (this.aXcY / this.aXcX)); + } + + internal override void Apply(Span scanline, int x, int y) + { + base.Apply(scanline, x, y); + + // Span destinationRow = this.Target.GetPixelRowSpan(y).Slice(x, scanline.Length); + // MemoryManager memoryManager = this.Target.MemoryManager; + // using (IBuffer amountBuffer = memoryManager.Allocate(scanline.Length)) + // { + // Span amountSpan = amountBuffer.Span; + // + // for (int i = 0; i < scanline.Length; i++) + // { + // amountSpan[i] = scanline[i] * this.Options.BlendPercentage; + // } + // + // 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 new file mode 100644 index 0000000000..3d613adc05 --- /dev/null +++ b/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Drawing; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Drawing +{ + using System; + + using SixLabors.ImageSharp.Processing; + using SixLabors.ImageSharp.Processing.Drawing.Brushes; + using SixLabors.ImageSharp.Processing.Overlays; + + using Point = SixLabors.Primitives.Point; + + public class FillLinearGradientBrushTests : FileTestBase + { + [Fact] + public void LinearGradientBrushWithEqualColorsReturnsUnicolorImage() + { + string path = TestEnvironment.CreateOutputDirectory("Fill", "LinearGradientBrush"); + using (var image = new Image(500, 500)) + { + LinearGradientBrush unicolorLinearGradientBrush = + new LinearGradientBrush( + new Point(0, 0), + new Point(500, 0), + new Tuple(0, Rgba32.Red), + new Tuple(1, Rgba32.Red)); + + image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); + image.Save($"{path}/UnicolorGradient.png"); + + using (PixelAccessor sourcePixels = image.Lock()) + { + Assert.Equal(Rgba32.Red, sourcePixels[0, 0]); + Assert.Equal(Rgba32.Red, sourcePixels[9, 9]); + Assert.Equal(Rgba32.Red, sourcePixels[199, 149]); + Assert.Equal(Rgba32.Red, sourcePixels[500, 500]); + } + } + } + } +}