// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
using SixLabors.ImageSharp.Drawing.Helpers;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Drawing.Processing {
///
/// A radial gradient brush defined by either one circle or two circles.
/// When one circle is provided, the gradient parameter is the distance from the center divided by the radius.
/// When two circles are provided, the gradient parameter is computed along the family of circles interpolating
/// between the start and end circles.
///
public sealed class RadialGradientBrush : GradientBrush
{
///
/// Initializes a new instance of the class using a single circle.
///
/// The center of the circular gradient.
/// The radius of the circular gradient.
/// Defines how the colors in the gradient are repeated.
/// The ordered gradient stops.
public RadialGradientBrush(
PointF center,
float radius,
GradientRepetitionMode repetitionMode,
params ColorStop[] colorStops)
: base(repetitionMode, colorStops)
{
this.Center0 = center;
this.Radius0 = radius;
this.Center1 = null;
this.Radius1 = null;
}
///
/// Initializes a new instance of the class using two circles.
///
/// The center of the starting circle.
/// The radius of the starting circle.
/// The center of the ending circle.
/// The radius of the ending circle.
/// Defines how the colors in the gradient are repeated.
/// The ordered gradient stops.
public RadialGradientBrush(
PointF startCenter,
float startRadius,
PointF endCenter,
float endRadius,
GradientRepetitionMode repetitionMode,
params ColorStop[] colorStops)
: base(repetitionMode, colorStops)
{
this.Center0 = startCenter;
this.Radius0 = startRadius;
this.Center1 = endCenter;
this.Radius1 = endRadius;
}
///
/// Gets the center of the starting circle.
///
public PointF Center0 { get; }
///
/// Gets the radius of the starting circle.
///
public float Radius0 { get; }
///
/// Gets the center of the ending circle, or for single-circle form.
///
public PointF? Center1 { get; }
///
/// Gets the radius of the ending circle, or for single-circle form.
///
public float? Radius1 { get; }
///
/// Gets a value indicating whether this is a two-circle radial gradient.
///
public bool IsTwoCircle => this.Center1.HasValue && this.Radius1.HasValue;
///
public override Brush Transform(Matrix4x4 matrix)
{
PointF tc0 = PointF.Transform(this.Center0, matrix);
float scale = MatrixUtilities.GetAverageScale(in matrix);
if (this.IsTwoCircle)
{
PointF tc1 = PointF.Transform(this.Center1!.Value, matrix);
return new RadialGradientBrush(tc0, this.Radius0 * scale, tc1, this.Radius1!.Value * scale, this.RepetitionMode, this.ColorStopsArray);
}
return new RadialGradientBrush(tc0, this.Radius0 * scale, this.RepetitionMode, this.ColorStopsArray);
}
///
public override bool Equals(Brush? other)
{
if (other is RadialGradientBrush b)
{
return base.Equals(other)
&& this.Center0.Equals(b.Center0)
&& this.Radius0.Equals(b.Radius0)
&& Nullable.Equals(this.Center1, b.Center1)
&& Nullable.Equals(this.Radius1, b.Radius1);
}
return false;
}
///
public override int GetHashCode()
=> HashCode.Combine(base.GetHashCode(), this.Center0, this.Radius0, this.Center1, this.Radius1);
///
public override BrushRenderer CreateRenderer(
Configuration configuration,
GraphicsOptions options,
int canvasWidth,
RectangleF region)
=> new RadialGradientBrushRenderer(
configuration,
options,
canvasWidth,
this.Center0,
this.Radius0,
this.Center1,
this.Radius1,
this.ColorStopsArray,
this.RepetitionMode);
///
/// The radial gradient brush applicator.
///
private sealed class RadialGradientBrushRenderer : GradientBrushRenderer
where TPixel : unmanaged, IPixel
{
private const float GradientEpsilon = 1F / (1 << 12);
// Single-circle fields
private readonly bool isTwoCircle;
private readonly float c0x;
private readonly float c0y;
private readonly float r0;
// Two-circle gradient fields.
// The transform changes coordinates so the gradient can be evaluated
// with simple formulas around a canonical line/circle configuration.
private readonly Matrix3x2 radialTransform;
private readonly float focalX;
private readonly float radius;
private readonly bool isStrip;
private readonly bool isCircular;
private readonly bool isFocalOnCircle;
private readonly bool isSwapped;
///
/// 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.
/// Center of the starting circle.
/// Radius of the starting circle.
/// Center of the ending circle, or null to use single-circle form.
/// Radius of the ending circle, or null to use single-circle form.
/// Definition of colors.
/// How the colors are repeated beyond the first gradient.
public RadialGradientBrushRenderer(
Configuration configuration,
GraphicsOptions options,
int canvasWidth,
PointF center0,
float radius0,
PointF? center1,
float? radius1,
ColorStop[] colorStops,
GradientRepetitionMode repetitionMode)
: base(configuration, options, canvasWidth, colorStops, repetitionMode)
{
this.c0x = center0.X;
this.c0y = center0.Y;
this.r0 = radius0;
this.isTwoCircle = center1.HasValue && radius1.HasValue;
if (this.isTwoCircle)
{
ConicalGradientParameters parameters = CreateConicalGradientParameters(
center0,
radius0,
center1!.Value,
radius1!.Value);
this.radialTransform = parameters.Transform;
this.focalX = parameters.FocalX;
this.radius = parameters.Radius;
this.isStrip = parameters.IsStrip;
this.isCircular = parameters.IsCircular;
this.isFocalOnCircle = parameters.IsFocalOnCircle;
this.isSwapped = parameters.IsSwapped;
}
else
{
this.radialTransform = Matrix3x2.Identity;
this.focalX = 0F;
this.radius = 0F;
this.isStrip = false;
this.isCircular = false;
this.isFocalOnCircle = false;
this.isSwapped = false;
}
}
///
protected override float PositionOnGradient(float x, float y)
{
if (!this.isTwoCircle)
{
float ux = x - this.c0x, uy = y - this.c0y;
return MathF.Sqrt((ux * ux) + (uy * uy)) / this.r0;
}
// Move the sample into the canonical coordinate system where the
// end circle lies on the x-axis and the conic can be solved using
// closed-form expressions.
Vector2 local = Vector2.Transform(new Vector2(x, y), this.radialTransform);
float localX = local.X;
float localY = local.Y;
float xx = localX * localX;
float yy = localY * localY;
float t;
if (this.isStrip)
{
// Strip gradients are bounded by a band around the axis.
// radius stores the squared half-width in normalized space,
// so points outside the band are invalid.
float a = this.radius - yy;
if (a < 0F)
{
return float.NaN;
}
// Once inside the band, the parameter advances along the axis.
t = MathF.Sqrt(a) + localX;
}
else if (this.isFocalOnCircle)
{
// This degenerate case reduces to a rational expression where
// the focal point sits exactly on the limiting circle.
if (localX == 0F)
{
return float.NaN;
}
t = (xx + yy) / localX;
if (t < 0F)
{
return float.NaN;
}
}
else if (this.radius > 1F)
{
// Wide cones use a circular norm. The x term shifts the root
// back into the original gradient parameterization.
float radiusReciprocal = this.isCircular ? 0F : 1F / this.radius;
t = MathF.Sqrt(xx + yy) - (localX * radiusReciprocal);
}
else
{
// Narrow cones use a hyperbolic form. Points with x^2 < y^2
// lie outside the valid branch and must not contribute.
float a = xx - yy;
if (a < 0F)
{
return float.NaN;
}
// lessScale picks the correct branch of the hyperbola after
// swaps and orientation changes.
float lessScale = (this.isSwapped || (1F - this.focalX) < 0F) ? -1F : 1F;
t = (lessScale * MathF.Sqrt(a)) - (localX / this.radius);
if (t < 0F)
{
return float.NaN;
}
}
// Convert back from the normalized local solution into the brush's
// gradient parameter, then undo the earlier swap if required.
t = this.focalX + (MathF.Sign(1F - this.focalX) * t);
return this.isSwapped ? 1F - t : t;
}
private static ConicalGradientParameters CreateConicalGradientParameters(
PointF center0,
float radius0,
PointF center1,
float radius1)
{
PointF p0 = center0;
PointF p1 = center1;
float r0 = radius0;
float r1 = radius1;
if (MathF.Abs(r0 - r1) <= GradientEpsilon)
{
// When both circles have the same radius, the locus becomes a
// strip: solve along the axis between the centers, with the
// radius contributing only a perpendicular cutoff.
float scaled = r0 / Distance(p0, p1);
return new ConicalGradientParameters(
TwoPointToUnitLine(p0, p1),
0F,
scaled * scaled,
isStrip: true,
isCircular: false,
isFocalOnCircle: false,
isSwapped: false);
}
bool isCircular = false;
if (p0 == p1)
{
isCircular = true;
// Equal centers make the conic circular. Nudge slightly so the
// line construction below stays invertible.
p0 = new PointF(p0.X + GradientEpsilon, p0.Y + GradientEpsilon);
}
bool isSwapped = false;
if (r1 == 0F)
{
isSwapped = true;
// Put the zero-radius focus on the start side so the later
// formulas keep one orientation.
(p0, p1) = (p1, p0);
(r0, r1) = (r1, r0);
}
// focalX describes where the focal point lies along the line from
// the start circle to the end circle. Values outside [0, 1] are
// valid and correspond to cones whose focus lies beyond an endpoint.
float focalX = r0 / (r0 - r1);
PointF cf = new(
((1F - focalX) * p0.X) + (focalX * p1.X),
((1F - focalX) * p0.Y) + (focalX * p1.Y));
// radius is the end-circle radius expressed in the normalized frame
// built from the focal point and the end center.
float radius = r1 / Distance(cf, p1);
Matrix3x2 userToUnitLine = TwoPointToUnitLine(cf, p1);
Matrix3x2 transform;
bool isFocalOnCircle = false;
if (MathF.Abs(radius - 1F) <= GradientEpsilon)
{
isFocalOnCircle = true;
// When the focal point lies on the circle, the quadratic terms
// collapse to a simpler rational form.
float scale = 0.5F * MathF.Abs(1F - focalX);
transform = userToUnitLine * Matrix3x2.CreateScale(scale);
}
else
{
// Otherwise scale the unit-line frame so the gradient can be
// tested with either x^2 + y^2 or x^2 - y^2, depending on
// whether the cone opens wider or narrower than the unit case.
float a = (radius * radius) - 1F;
float scaleRatio = MathF.Abs(1F - focalX) / a;
float scaleX = radius * scaleRatio;
float scaleY = MathF.Sqrt(MathF.Abs(a)) * scaleRatio;
transform = userToUnitLine * Matrix3x2.CreateScale(scaleX, scaleY);
}
return new ConicalGradientParameters(
transform,
focalX,
radius,
isStrip: false,
isCircular: isCircular,
isFocalOnCircle: isFocalOnCircle,
isSwapped: isSwapped);
}
private static float Distance(Vector2 p0, Vector2 p1) => Vector2.Distance(p0, p1);
private static Matrix3x2 TwoPointToUnitLine(PointF p0, PointF p1)
{
// Build a change-of-basis that sends the segment p0->p1 to the
// unit line. That lets the gradient math work in one fixed frame
// instead of re-deriving equations for every brush.
Matrix3x2 source = FromPoly2(p0, p1);
Matrix3x2.Invert(source, out Matrix3x2 inverse);
return inverse * FromPoly2(new PointF(0F, 0F), new PointF(1F, 0F));
}
private static Matrix3x2 FromPoly2(PointF p0, PointF p1)
// This affine frame uses p0 as the origin and p0->p1 as one axis.
// Its inverse is the basis change we need for normalization.
=> new(
p1.Y - p0.Y,
p0.X - p1.X,
p1.X - p0.X,
p1.Y - p0.Y,
p0.X,
p0.Y);
private readonly struct ConicalGradientParameters
{
public ConicalGradientParameters(
Matrix3x2 transform,
float focalX,
float radius,
bool isStrip,
bool isCircular,
bool isFocalOnCircle,
bool isSwapped)
{
this.Transform = transform;
this.FocalX = focalX;
this.Radius = radius;
this.IsStrip = isStrip;
this.IsCircular = isCircular;
this.IsFocalOnCircle = isFocalOnCircle;
this.IsSwapped = isSwapped;
}
public Matrix3x2 Transform { get; }
public float FocalX { get; }
public float Radius { get; }
public bool IsStrip { get; }
public bool IsCircular { get; }
public bool IsFocalOnCircle { get; }
public bool IsSwapped { get; }
}
}
}
}