// 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 { /// /// Provides an implementation of a brush for painting sweep (conic) gradients within areas. /// Angles increase counter-clockwise from +X on the design grid. /// public sealed class SweepGradientBrush : GradientBrush { /// /// Initializes a new instance of the class. /// /// The center point of the sweep gradient in device space. /// /// The starting angle, in degrees, measured counter-clockwise from +X on the design grid. /// This value is stored as provided so the sign and magnitude of the sweep remain intact. /// /// /// The ending angle, in degrees, measured counter-clockwise from +X on the design grid. /// If equal to , the gradient is treated as a full 360 degree sweep. /// Otherwise, the signed difference between start and end determines the sweep direction. /// /// Defines how the gradient colors are repeated beyond the interval [0..1]. /// The gradient color stops. Ratios must be in [0..1] and are interpreted along the angular sweep. public SweepGradientBrush( PointF center, float startAngleDegrees, float endAngleDegrees, GradientRepetitionMode repetitionMode, params ColorStop[] colorStops) : base(repetitionMode, colorStops) { this.Center = center; this.StartAngleDegrees = startAngleDegrees; this.EndAngleDegrees = endAngleDegrees; } /// /// Gets the center point of the sweep gradient. /// public PointF Center { get; } /// /// Gets the starting angle in degrees. /// public float StartAngleDegrees { get; } /// /// Gets the ending angle in degrees. /// public float EndAngleDegrees { get; } /// public override Brush Transform(Matrix4x4 matrix) { PointF tc = PointF.Transform(this.Center, matrix); // Treat the brush as two rays starting at the center: // one ray for the start angle and one ray for the end angle. // The important value is the signed angular distance between those rays. // We keep that sign so a reflected transform can turn a counter-clockwise // sweep into a clockwise sweep instead of silently "fixing" it. float sweepDegrees = GetEffectiveSweepDegrees(this.StartAngleDegrees, this.EndAngleDegrees); float startRad = GeometryUtilities.DegreeToRadian(this.StartAngleDegrees); float endRad = GeometryUtilities.DegreeToRadian(this.StartAngleDegrees + sweepDegrees); // The public API uses the design-grid convention, which is y-up. // Screen pixels are y-down, so a positive mathematical rotation uses // `center.Y - sin(theta)` rather than `center.Y + sin(theta)`. PointF startDir = PointF.Transform(new PointF(this.Center.X + MathF.Cos(startRad), this.Center.Y - MathF.Sin(startRad)), matrix); PointF endDir = PointF.Transform(new PointF(this.Center.X + MathF.Cos(endRad), this.Center.Y - MathF.Sin(endRad)), matrix); // Convert the transformed rays back into brush angles in the same public convention: // counter-clockwise from +X on the design grid. float newStart = NormalizeDirectionDegrees(MathF.Atan2(-(startDir.Y - tc.Y), startDir.X - tc.X) * (180f / MathF.PI)); float newEnd = NormalizeDirectionDegrees(MathF.Atan2(-(endDir.Y - tc.Y), endDir.X - tc.X) * (180f / MathF.PI)); // A negative determinant means the transform flips orientation. // That flips the direction of the sweep, so we use it to decide whether // the end angle should unwrap forwards or backwards from the new start. float determinant = (matrix.M11 * matrix.M22) - (matrix.M12 * matrix.M21); float directionHint = MathF.Sign(sweepDegrees); if (directionHint == 0F) { directionHint = 1F; } if (determinant < 0F) { directionHint = -directionHint; } return new SweepGradientBrush( tc, newStart, UnwrapSweepEndDegrees(newStart, newEnd, directionHint, MathF.Abs(sweepDegrees)), this.RepetitionMode, this.ColorStopsArray); } /// public override bool Equals(Brush? other) { // Sweep brushes are equal only when they describe the same center, // the same signed angular interval, and the same inherited stop data. if (other is SweepGradientBrush brush) { return base.Equals(other) && this.Center.Equals(brush.Center) && this.StartAngleDegrees.Equals(brush.StartAngleDegrees) && this.EndAngleDegrees.Equals(brush.EndAngleDegrees); } return false; } /// public override int GetHashCode() => HashCode.Combine( base.GetHashCode(), this.Center, this.StartAngleDegrees, this.EndAngleDegrees); /// /// Converts the stored start/end angles into the signed sweep interval that the brush should render. /// /// The starting angle in degrees. /// The ending angle in degrees. /// /// The signed angular interval in degrees. Equal endpoints are treated as a full turn. /// // Sweep gradients interpret equal endpoints as "full turn". // All other cases keep the caller-provided signed angular span. private static float GetEffectiveSweepDegrees(float startAngleDegrees, float endAngleDegrees) { float sweepDegrees = endAngleDegrees - startAngleDegrees; if (MathF.Abs(sweepDegrees) < 1e-6F) { // Equal endpoints mean "full circle", not an empty span. return 360F; } return sweepDegrees; } /// /// Normalizes an angle to the canonical [0, 360) direction range. /// /// The angle to normalize. /// The equivalent direction in the canonical degree range. // Convert any equivalent direction into the canonical [0, 360) representation // so transformed brushes remain stable when compared or reused. private static float NormalizeDirectionDegrees(float degrees) { float normalized = degrees % 360F; if (normalized < 0F) { normalized += 360F; } return normalized; } /// /// Reconstructs the signed end angle after independently transforming the start and end rays. /// /// The transformed starting angle in normalized degrees. /// The transformed ending angle in normalized degrees. /// /// The expected sweep direction. Positive means unwrap forwards, negative means unwrap backwards. /// /// The minimum magnitude the restored interval must preserve. /// The unwrapped ending angle measured relative to . // After transforming the start and end rays separately, both directions land in [0, 360). // This method restores the intended signed sweep by unwrapping the end angle relative to // the start angle, using the desired direction as the constraint. private static float UnwrapSweepEndDegrees(float startDegrees, float endDegrees, float directionHint, float minimumMagnitude) { float delta = endDegrees - startDegrees; if (directionHint >= 0F) { // Keep the end angle ahead of the start angle for a positive sweep. while (delta < 0F) { delta += 360F; } if (MathF.Abs(delta) < 1e-6F && minimumMagnitude >= 360F - 1e-6F) { delta = 360F; } } else { // Keep the end angle behind the start angle for a negative sweep. while (delta > 0F) { delta -= 360F; } if (MathF.Abs(delta) < 1e-6F && minimumMagnitude >= 360F - 1e-6F) { delta = -360F; } } return startDegrees + delta; } /// public override BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, int canvasWidth, RectangleF region) => // The renderer precomputes the angular interval once and then samples it per pixel. new SweepGradientBrushRenderer( configuration, options, canvasWidth, this, this.ColorStopsArray, this.RepetitionMode); /// /// The sweep (conic) gradient brush applicator. /// /// The pixel format. private sealed class SweepGradientBrushRenderer : GradientBrushRenderer where TPixel : unmanaged, IPixel { private const float Tau = MathF.Tau; private readonly float cx; private readonly float cy; private readonly float startRad; private readonly float endRad; /// /// 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. /// The sweep gradient brush. /// The gradient color stops (ratios in [0..1]). /// Defines how gradient colors are repeated outside [0..1]. public SweepGradientBrushRenderer( Configuration configuration, GraphicsOptions options, int canvasWidth, SweepGradientBrush brush, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) : base(configuration, options, canvasWidth, colorStops, repetitionMode) { this.cx = brush.Center.X; this.cy = brush.Center.Y; // Store the interval as radians once so sampling only needs one subtraction and one divide. float sweepDegrees = GetEffectiveSweepDegrees(brush.StartAngleDegrees, brush.EndAngleDegrees); this.startRad = GeometryUtilities.DegreeToRadian(brush.StartAngleDegrees); this.endRad = GeometryUtilities.DegreeToRadian(brush.StartAngleDegrees + sweepDegrees); } /// protected override float PositionOnGradient(float x, float y) { // Move the sample into center-relative coordinates. float dx = x - this.cx; float dy = y - this.cy; if (dx == 0f && dy == 0f) { // The center has no unique angle, so pick a stable value on the gradient. return 0f; } // Convert from y-down image space back into the brush's y-up angle convention, // then normalize to [0, 2π) so subtraction against the stored start angle is stable. float angle = MathF.Atan2(-dy, dx); if (angle < 0f) { angle += Tau; } // Divide by the signed angular span. // A positive denominator produces a counter-clockwise sweep and a negative // denominator produces a clockwise sweep. The base gradient code then applies // the repetition mode to this unbounded parameter. return (angle - this.startRad) / (this.endRad - this.startRad); } } } }