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 { /// /// 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 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) { 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; } } /// /// The linear gradient brush applicator. /// private class LinearGradientBrushApplicator : BrushApplicator { private readonly Point start; private readonly Point end; private readonly ColorStop[] 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; /// /// the result of ^2 + ^2 /// private readonly float alongsSquared; /// /// the length of the defined gradient (between source and end) /// private readonly float length; /// /// 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, ColorStop[] 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.end.X - this.start.X; this.alongY = this.end.Y - this.start.Y; // the cross vector: this.acrossX = this.alongY; this.acrossY = -this.alongX; // some helpers: this.alongsSquared = (this.alongX * this.alongX) + (this.alongY * this.alongY); 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) { if (this.acrossX == 0) { return (x - this.start.X) / (float)(this.end.X - this.start.X); } else if (this.acrossY == 0) { return (y - this.start.Y) / (float)(this.end.Y - this.start.Y); } else { float deltaX = x - this.start.X; float deltaY = y - this.start.Y; float k = ((this.alongY * deltaX) - (this.alongX * deltaY)) / this.alongsSquared; // point on the line: float x4 = x - (k * this.alongY); float y4 = y + (k * this.alongX); // get distance from (x4,y4) to start float distance = (float)Math.Sqrt( Math.Pow(x4 - this.start.X, 2) + Math.Pow(y4 - this.start.Y, 2)); // get and return ratio float ratio = distance / this.length; return ratio; } } 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() { } } } }