// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using SixLabors.ImageSharp.Drawing.Helpers;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Drawing.Processing {
///
/// Provides an implementation of a brush for painting gradients between multiple color positions in 2D coordinates.
///
public sealed class PathGradientBrush : Brush
{
private readonly PointF[] points;
private readonly Color[] colors;
private readonly Edge[] edges;
///
/// Initializes a new instance of the class.
///
/// Points that constitute a polygon that represents the gradient area.
/// Array of colors that correspond to each point in the polygon.
public PathGradientBrush(PointF[] points, Color[] colors)
{
Guard.NotNull(points, nameof(points));
Guard.MustBeGreaterThanOrEqualTo(points.Length, 3, nameof(points));
Guard.NotNull(colors, nameof(colors));
Guard.MustBeGreaterThan(colors.Length, 0, nameof(colors));
int size = points.Length;
this.points = [.. points];
this.colors = [.. colors];
this.edges = new Edge[this.points.Length];
for (int i = 0; i < this.points.Length; i++)
{
this.edges[i] = new Edge(this.points[i % size], this.points[(i + 1) % size], ColorAt(i), ColorAt(i + 1));
}
this.CenterColor = CalculateCenterColor(this.colors);
Color ColorAt(int index) => this.colors[index % this.colors.Length];
}
///
/// Initializes a new instance of the class.
///
/// Points that constitute a polygon that represents the gradient area.
/// Array of colors that correspond to each point in the polygon.
/// Color at the center of the gradient area to which the other colors converge.
public PathGradientBrush(PointF[] points, Color[] colors, Color centerColor)
: this(points, colors)
{
this.CenterColor = centerColor;
this.HasExplicitCenterColor = true;
}
///
/// Gets the polygon points that define the gradient area.
///
public ReadOnlySpan Points => this.points;
///
/// Gets the colors that are mapped to the polygon points.
///
public ReadOnlySpan Colors => this.colors;
///
/// Gets the color at the center of the gradient area.
///
public Color CenterColor { get; }
///
/// Gets a value indicating whether the center color was explicitly supplied.
///
public bool HasExplicitCenterColor { get; }
///
public override Brush Transform(Matrix4x4 matrix)
{
if (matrix.IsIdentity)
{
return this;
}
PointF[] transformedPoints = new PointF[this.points.Length];
for (int i = 0; i < transformedPoints.Length; i++)
{
transformedPoints[i] = PointF.Transform(this.points[i], matrix);
}
return this.HasExplicitCenterColor
? new PathGradientBrush(transformedPoints, this.colors, this.CenterColor)
: new PathGradientBrush(transformedPoints, this.colors);
}
///
public override bool Equals(Brush? other)
{
if (other is PathGradientBrush brush)
{
return this.CenterColor.Equals(brush.CenterColor)
&& this.HasExplicitCenterColor.Equals(brush.HasExplicitCenterColor)
&& this.edges?.SequenceEqual(brush.edges) == true;
}
return false;
}
///
public override int GetHashCode()
=> HashCode.Combine(this.edges, this.CenterColor, this.HasExplicitCenterColor);
///
public override BrushRenderer CreateRenderer(
Configuration configuration,
GraphicsOptions options,
int canvasWidth,
RectangleF region)
=> new PathGradientBrushRenderer(
configuration,
options,
canvasWidth,
this.edges,
this.CenterColor,
this.HasExplicitCenterColor);
private static Color CalculateCenterColor(Color[] colors)
{
Guard.NotNull(colors, nameof(colors));
Guard.MustBeGreaterThan(colors.Length, 0, nameof(colors));
return Color.FromScaledVector(colors.Select(c => c.ToScaledVector4()).Aggregate((p1, p2) => p1 + p2) / colors.Length);
}
private static float DistanceBetween(Vector2 p1, Vector2 p2) => (p2 - p1).Length();
private readonly struct Intersection
{
public Intersection(PointF point, float distance)
{
this.Point = point;
this.Distance = distance;
}
public PointF Point { get; }
public float Distance { get; }
}
///
/// An edge of the polygon that represents the gradient area.
///
private class Edge : IEquatable
{
private readonly float length;
public Edge(Vector2 start, Vector2 end, Color startColor, Color endColor)
{
this.Start = start;
this.End = end;
this.StartColor = startColor.ToScaledVector4();
this.EndColor = endColor.ToScaledVector4();
this.length = DistanceBetween(this.End, this.Start);
}
public Vector2 Start { get; }
public Vector2 End { get; }
public Vector4 StartColor { get; }
public Vector4 EndColor { get; }
public bool Intersect(
Vector2 start,
Vector2 end,
ref Vector2 ip) =>
PolygonUtilities.LineSegmentToLineSegmentIgnoreCollinear(start, end, this.Start, this.End, ref ip);
public Vector4 ColorAt(float distance)
{
float ratio = this.length > 0 ? distance / this.length : 0;
return Vector4.Lerp(this.StartColor, this.EndColor, ratio);
}
public Vector4 ColorAt(PointF point) => this.ColorAt(DistanceBetween(point, this.Start));
public bool Equals(Edge? other)
=> other != null &&
other.Start == this.Start &&
other.End == this.End &&
other.StartColor.Equals(this.StartColor) &&
other.EndColor.Equals(this.EndColor);
public override bool Equals(object? obj) => this.Equals(obj as Edge);
public override int GetHashCode()
=> HashCode.Combine(this.Start, this.End, this.StartColor, this.EndColor);
}
///
/// The path gradient brush applicator.
///
/// The pixel format.
private sealed class PathGradientBrushRenderer : BrushRenderer
where TPixel : unmanaged, IPixel
{
private readonly Vector2 center;
private readonly Vector4 centerColor;
private readonly bool hasSpecialCenterColor;
private readonly float maxDistance;
private readonly IList edges;
private readonly TPixel centerPixel;
private readonly TPixel transparentPixel;
///
/// 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.
/// Edges of the polygon.
/// Color at the center of the gradient area to which the other colors converge.
/// Whether the center color is different from a smooth gradient between the edges.
public PathGradientBrushRenderer(
Configuration configuration,
GraphicsOptions options,
int canvasWidth,
IList edges,
Color centerColor,
bool hasSpecialCenterColor)
: base(configuration, options, canvasWidth)
{
this.edges = edges;
Vector2[] points = [.. edges.Select(s => s.Start)];
this.center = points.Aggregate((p1, p2) => p1 + p2) / edges.Count;
this.centerColor = centerColor.ToScaledVector4();
this.hasSpecialCenterColor = hasSpecialCenterColor;
this.centerPixel = centerColor.ToPixel();
this.maxDistance = points.Select(p => p - this.center).Max(d => d.Length());
this.transparentPixel = Color.Transparent.ToPixel();
}
internal TPixel this[int x, int y]
{
get
{
// Match other gradient brushes by evaluating at pixel centers.
Vector2 point = new(x + 0.5F, y + 0.5F);
if (point == this.center)
{
return this.centerPixel;
}
if (this.edges.Count == 3 && !this.hasSpecialCenterColor)
{
if (!FindPointOnTriangle(
this.edges[0].Start,
this.edges[1].Start,
this.edges[2].Start,
point,
out float u,
out float v))
{
return this.transparentPixel;
}
Vector4 pointColor = ((1 - u - v) * this.edges[0].StartColor)
+ (u * this.edges[0].EndColor)
+ (v * this.edges[2].StartColor);
return TPixel.FromScaledVector4(pointColor);
}
Vector2 direction = Vector2.Normalize(point - this.center);
Vector2 end = point + (direction * this.maxDistance);
(Edge Edge, Vector2 Point)? isc = this.FindIntersection(point, end);
if (!isc.HasValue)
{
return this.transparentPixel;
}
Vector2 intersection = isc.Value.Point;
Vector4 edgeColor = isc.Value.Edge.ColorAt(intersection);
float length = DistanceBetween(intersection, this.center);
float ratio = length > 0 ? DistanceBetween(intersection, point) / length : 0;
Vector4 color = Vector4.Lerp(edgeColor, this.centerColor, ratio);
return TPixel.FromScaledVector4(color);
}
}
///
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));
}
private (Edge Edge, Vector2 Point)? FindIntersection(
PointF start,
PointF end)
{
Vector2 ip = default;
Vector2 closestIntersection = default;
Edge? closestEdge = null;
float minDistance = float.MaxValue;
foreach (Edge edge in this.edges)
{
if (!edge.Intersect(start, end, ref ip))
{
continue;
}
float d = Vector2.DistanceSquared(start, ip);
if (d < minDistance)
{
minDistance = d;
closestEdge = edge;
closestIntersection = ip;
}
}
return closestEdge != null ? (closestEdge, closestIntersection) : null;
}
private static bool FindPointOnTriangle(Vector2 v1, Vector2 v2, Vector2 v3, Vector2 point, out float u, out float v)
{
Vector2 e1 = v2 - v1;
Vector2 e2 = v3 - v2;
Vector2 e3 = v1 - v3;
Vector2 pv1 = point - v1;
Vector2 pv2 = point - v2;
Vector2 pv3 = point - v3;
Vector3 d1 = Vector3.Cross(new Vector3(e1.X, e1.Y, 0), new Vector3(pv1.X, pv1.Y, 0));
Vector3 d2 = Vector3.Cross(new Vector3(e2.X, e2.Y, 0), new Vector3(pv2.X, pv2.Y, 0));
Vector3 d3 = Vector3.Cross(new Vector3(e3.X, e3.Y, 0), new Vector3(pv3.X, pv3.Y, 0));
if (Math.Sign(Vector3.Dot(d1, d2)) * Math.Sign(Vector3.Dot(d1, d3)) == -1 || Math.Sign(Vector3.Dot(d1, d2)) * Math.Sign(Vector3.Dot(d2, d3)) == -1)
{
u = 0;
v = 0;
return false;
}
// From Real-Time Collision Detection
// https://gamedev.stackexchange.com/questions/23743/whats-the-most-efficient-way-to-find-barycentric-coordinates
float d00 = Vector2.Dot(e1, e1);
float d01 = -Vector2.Dot(e1, e3);
float d11 = Vector2.Dot(e3, e3);
float d20 = Vector2.Dot(pv1, e1);
float d21 = -Vector2.Dot(pv1, e3);
float denominator = (d00 * d11) - (d01 * d01);
u = ((d11 * d20) - (d01 * d21)) / denominator;
v = ((d00 * d21) - (d01 * d20)) / denominator;
return true;
}
}
}
}