// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.PixelFormats; using System; using System.Numerics; namespace SixLabors.ImageSharp.Drawing.Processing { /// /// Base class for Gradient brushes /// public abstract class GradientBrush : Brush { /// /// Defines how the colors are repeated beyond the interval [0..1] /// The gradient colors. protected GradientBrush(GradientRepetitionMode repetitionMode, params ColorStop[] colorStops) { this.RepetitionMode = repetitionMode; InsertionSort(colorStops, (a, b) => a.Ratio.CompareTo(b.Ratio)); this.ColorStopsArray = colorStops; } /// /// Gets how the colors are repeated beyond the interval [0..1]. /// public GradientRepetitionMode RepetitionMode { get; } /// /// Gets the color stops for this gradient. /// public ReadOnlySpan ColorStops => this.ColorStopsArray; /// /// Gets the color stops array for use by derived applicators. /// protected ColorStop[] ColorStopsArray { get; } /// public override bool Equals(Brush? other) { if (other is GradientBrush brush) { return this.RepetitionMode == brush.RepetitionMode && this.ColorStopsArray?.SequenceEqual(brush.ColorStopsArray) == true; } return false; } /// public override int GetHashCode() => HashCode.Combine(this.RepetitionMode, this.ColorStopsArray); /// /// Sorts the collection in place using a stable insertion sort. /// is not stable and can reorder /// equal-ratio color stops, producing non-deterministic gradient results. /// private static void InsertionSort(T[] collection, Comparison comparison) { int count = collection.Length; for (int j = 1; j < count; j++) { T key = collection[j]; int i = j - 1; for (; i >= 0 && comparison(collection[i], key) > 0; i--) { collection[i + 1] = collection[i]; } collection[i + 1] = key; } } /// /// Base class for gradient brush applicators /// /// The pixel format. internal abstract class GradientBrushRenderer : BrushRenderer where TPixel : unmanaged, IPixel { private static readonly TPixel Transparent = Color.Transparent.ToPixel(); private readonly ColorStop[] colorStops; private readonly GradientRepetitionMode repetitionMode; /// /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. /// The canvas width for the current render pass. /// An array of color stops sorted by their position. /// Defines if and how the gradient should be repeated. protected GradientBrushRenderer( Configuration configuration, GraphicsOptions options, int canvasWidth, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) : base(configuration, options, canvasWidth) { this.colorStops = colorStops; this.repetitionMode = repetitionMode; } internal TPixel this[int x, int y] { get { float fx = x + 0.5f; float fy = y + 0.5f; float positionOnCompleteGradient = this.PositionOnGradient(fx, fy); if (float.IsNaN(positionOnCompleteGradient)) { return Transparent; } switch (this.repetitionMode) { case GradientRepetitionMode.Repeat: positionOnCompleteGradient %= 1; break; case GradientRepetitionMode.Reflect: positionOnCompleteGradient %= 2; if (positionOnCompleteGradient > 1) { positionOnCompleteGradient = 2 - positionOnCompleteGradient; } break; case GradientRepetitionMode.DontFill: if (positionOnCompleteGradient is > 1 or < 0) { return Transparent; } break; case GradientRepetitionMode.None: default: // do nothing. The following could be done, but is not necessary: // onLocalGradient = Math.Min(0, Math.Max(1, onLocalGradient)); break; } (ColorStop from, ColorStop to) = this.GetGradientSegment(positionOnCompleteGradient); if (from.Color.Equals(to.Color)) { return from.Color.ToPixel(); } float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio); // TODO: This should use premultiplied vectors to avoid bad blends e.g. red -> brown <- green. return Color.FromScaledVector( Vector4.Lerp( from.Color.ToScaledVector4(), to.Color.ToScaledVector4(), onLocalGradient)).ToPixel(); } } /// public override void Apply( Span destinationRow, ReadOnlySpan scanline, int x, int y, BrushWorkspace workspace) { Span amounts = workspace.GetAmounts(scanline.Length); Span overlays = workspace.GetOverlays(scanline.Length); float blendPercentage = this.Options.BlendPercentage; // TODO: Remove bounds checks. if (blendPercentage < 1) { for (int i = 0; i < scanline.Length; i++) { amounts[i] = scanline[i] * blendPercentage; overlays[i] = this[x + i, y]; } } else { for (int i = 0; i < scanline.Length; i++) { amounts[i] = scanline[i]; overlays[i] = this[x + i, y]; } } this.Blender.Blend( this.Configuration, destinationRow, destinationRow, overlays, amounts, workspace.GetBlendScratch(scanline.Length, 3)); } /// /// Calculates the position on the gradient for a given point. /// This method is abstract as it's content depends on the shape of the gradient. /// /// The x-coordinate of the point. /// The y-coordinate of the point. /// /// The position the given point 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(float x, float y); private (ColorStop From, ColorStop To) GetGradientSegment(float positionOnCompleteGradient) { ColorStop localGradientFrom = this.colorStops[0]; ColorStop localGradientTo = default; foreach (ColorStop colorStop in this.colorStops) { localGradientTo = colorStop; if (colorStop.Ratio > positionOnCompleteGradient) { // we're done here, so break it! break; } localGradientFrom = localGradientTo; } return (localGradientFrom, localGradientTo); } } } }