mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
15 changed files with 1378 additions and 21 deletions
@ -0,0 +1,36 @@ |
|||||
|
using System.Diagnostics; |
||||
|
|
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A struct that defines a single color stop.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
[DebuggerDisplay("ColorStop({Ratio} -> {Color}")] |
||||
|
public struct ColorStop<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="ColorStop{TPixel}" /> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="ratio">Where should it be? 0 is at the start, 1 at the end of the Gradient.</param>
|
||||
|
/// <param name="color">What color should be used at that point?</param>
|
||||
|
public ColorStop(float ratio, TPixel color) |
||||
|
{ |
||||
|
this.Ratio = ratio; |
||||
|
this.Color = color; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the point along the defined gradient axis.
|
||||
|
/// </summary>
|
||||
|
public float Ratio { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the color to be used.
|
||||
|
/// </summary>
|
||||
|
public TPixel Color { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,167 @@ |
|||||
|
using System; |
||||
|
|
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gradient Brush with elliptic shape.
|
||||
|
/// The ellipse is defined by a center point,
|
||||
|
/// a point on the longest extension of the ellipse and
|
||||
|
/// the ratio between longest and shortest extension.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The Pixel format that is used.</typeparam>
|
||||
|
public sealed class EllipticGradientBrush<TPixel> : GradientBrushBase<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
private readonly Point center; |
||||
|
|
||||
|
private readonly Point referenceAxisEnd; |
||||
|
|
||||
|
private readonly float axisRatio; |
||||
|
|
||||
|
/// <inheritdoc cref="GradientBrushBase{TPixel}" />
|
||||
|
/// <param name="center">The center of the elliptical gradient and 0 for the color stops.</param>
|
||||
|
/// <param name="referenceAxisEnd">The end point of the reference axis of the ellipse.</param>
|
||||
|
/// <param name="axisRatio">
|
||||
|
/// The ratio of the axis widths.
|
||||
|
/// The second axis' is perpendicular to the reference axis and
|
||||
|
/// it's length is the reference axis' length multiplied by this factor.
|
||||
|
/// </param>
|
||||
|
/// <param name="repetitionMode">Defines how the colors of the gradients are repeated.</param>
|
||||
|
/// <param name="colorStops">the color stops as defined in base class.</param>
|
||||
|
public EllipticGradientBrush( |
||||
|
Point center, |
||||
|
Point referenceAxisEnd, |
||||
|
float axisRatio, |
||||
|
GradientRepetitionMode repetitionMode, |
||||
|
params ColorStop<TPixel>[] colorStops) |
||||
|
: base(repetitionMode, colorStops) |
||||
|
{ |
||||
|
this.center = center; |
||||
|
this.referenceAxisEnd = referenceAxisEnd; |
||||
|
this.axisRatio = axisRatio; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="CreateApplicator" />
|
||||
|
public override BrushApplicator<TPixel> CreateApplicator( |
||||
|
ImageFrame<TPixel> source, |
||||
|
RectangleF region, |
||||
|
GraphicsOptions options) => |
||||
|
new RadialGradientBrushApplicator( |
||||
|
source, |
||||
|
options, |
||||
|
this.center, |
||||
|
this.referenceAxisEnd, |
||||
|
this.axisRatio, |
||||
|
this.ColorStops, |
||||
|
this.RepetitionMode); |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
private sealed class RadialGradientBrushApplicator : GradientBrushApplicatorBase |
||||
|
{ |
||||
|
private readonly Point center; |
||||
|
|
||||
|
private readonly Point referenceAxisEnd; |
||||
|
|
||||
|
private readonly float axisRatio; |
||||
|
|
||||
|
private readonly double rotation; |
||||
|
|
||||
|
private readonly float referenceRadius; |
||||
|
|
||||
|
private readonly float secondRadius; |
||||
|
|
||||
|
private readonly float cosRotation; |
||||
|
|
||||
|
private readonly float sinRotation; |
||||
|
|
||||
|
private readonly float secondRadiusSquared; |
||||
|
|
||||
|
private readonly float referenceRadiusSquared; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="RadialGradientBrushApplicator" /> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="target">The target image</param>
|
||||
|
/// <param name="options">The options</param>
|
||||
|
/// <param name="center">Center of the ellipse</param>
|
||||
|
/// <param name="referenceAxisEnd">Point on one angular points of the ellipse.</param>
|
||||
|
/// <param name="axisRatio">
|
||||
|
/// Ratio of the axis length's. Used to determine the length of the second axis,
|
||||
|
/// the first is defined by <see cref="center"/> and <see cref="referenceAxisEnd"/>.</param>
|
||||
|
/// <param name="colorStops">Definition of colors</param>
|
||||
|
/// <param name="repetitionMode">Defines how the gradient colors are repeated.</param>
|
||||
|
public RadialGradientBrushApplicator( |
||||
|
ImageFrame<TPixel> target, |
||||
|
GraphicsOptions options, |
||||
|
Point center, |
||||
|
Point referenceAxisEnd, |
||||
|
float axisRatio, |
||||
|
ColorStop<TPixel>[] colorStops, |
||||
|
GradientRepetitionMode repetitionMode) |
||||
|
: base(target, options, colorStops, repetitionMode) |
||||
|
{ |
||||
|
this.center = center; |
||||
|
this.referenceAxisEnd = referenceAxisEnd; |
||||
|
this.axisRatio = axisRatio; |
||||
|
this.rotation = this.AngleBetween( |
||||
|
this.center, |
||||
|
new PointF(this.center.X + 1, this.center.Y), |
||||
|
this.referenceAxisEnd); |
||||
|
this.referenceRadius = this.DistanceBetween(this.center, this.referenceAxisEnd); |
||||
|
this.secondRadius = this.referenceRadius * this.axisRatio; |
||||
|
|
||||
|
this.referenceRadiusSquared = this.referenceRadius * this.referenceRadius; |
||||
|
this.secondRadiusSquared = this.secondRadius * this.secondRadius; |
||||
|
|
||||
|
this.sinRotation = (float)Math.Sin(this.rotation); |
||||
|
this.cosRotation = (float)Math.Cos(this.rotation); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override void Dispose() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override float PositionOnGradient(int xt, int yt) |
||||
|
{ |
||||
|
float x0 = xt - this.center.X; |
||||
|
float y0 = yt - this.center.Y; |
||||
|
|
||||
|
float x = (x0 * this.cosRotation) - (y0 * this.sinRotation); |
||||
|
float y = (x0 * this.sinRotation) + (y0 * this.cosRotation); |
||||
|
|
||||
|
float xSquared = x * x; |
||||
|
float ySquared = y * y; |
||||
|
|
||||
|
var inBoundaryChecker = (xSquared / this.referenceRadiusSquared) |
||||
|
+ (ySquared / this.secondRadiusSquared); |
||||
|
|
||||
|
return inBoundaryChecker; |
||||
|
} |
||||
|
|
||||
|
private float AngleBetween(PointF junction, PointF a, PointF b) |
||||
|
{ |
||||
|
var vA = a - junction; |
||||
|
var vB = b - junction; |
||||
|
return (float)(Math.Atan2(vB.Y, vB.X) |
||||
|
- Math.Atan2(vA.Y, vA.X)); |
||||
|
} |
||||
|
|
||||
|
private float DistanceBetween( |
||||
|
PointF p1, |
||||
|
PointF p2) |
||||
|
{ |
||||
|
float dX = p1.X - p2.X; |
||||
|
float dXsquared = dX * dX; |
||||
|
|
||||
|
float dY = p1.Y - p2.Y; |
||||
|
float dYsquared = dY * dY; |
||||
|
return (float)Math.Sqrt(dXsquared + dYsquared); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,174 @@ |
|||||
|
using System; |
||||
|
using System.Numerics; |
||||
|
|
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.PixelFormats.PixelBlenders; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Base class for Gradient brushes
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format</typeparam>
|
||||
|
public abstract class GradientBrushBase<TPixel> : IBrush<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
/// <inheritdoc cref="IBrush{TPixel}"/>
|
||||
|
/// <param name="repetitionMode">Defines how the colors are repeated beyond the interval [0..1]</param>
|
||||
|
/// <param name="colorStops">The gradient colors.</param>
|
||||
|
protected GradientBrushBase( |
||||
|
GradientRepetitionMode repetitionMode, |
||||
|
params ColorStop<TPixel>[] colorStops) |
||||
|
{ |
||||
|
this.RepetitionMode = repetitionMode; |
||||
|
this.ColorStops = colorStops; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets how the colors are repeated beyond the interval [0..1].
|
||||
|
/// </summary>
|
||||
|
protected GradientRepetitionMode RepetitionMode { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the list of color stops for this gradient.
|
||||
|
/// </summary>
|
||||
|
protected ColorStop<TPixel>[] ColorStops { get; } |
||||
|
|
||||
|
/// <inheritdoc cref="IBrush{TPixel}" />
|
||||
|
public abstract BrushApplicator<TPixel> CreateApplicator( |
||||
|
ImageFrame<TPixel> source, |
||||
|
RectangleF region, |
||||
|
GraphicsOptions options); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Base class for gradient brush applicators
|
||||
|
/// </summary>
|
||||
|
protected abstract class GradientBrushApplicatorBase : BrushApplicator<TPixel> |
||||
|
{ |
||||
|
private readonly ColorStop<TPixel>[] colorStops; |
||||
|
|
||||
|
private readonly GradientRepetitionMode repetitionMode; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="GradientBrushApplicatorBase"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="target">The target.</param>
|
||||
|
/// <param name="options">The options.</param>
|
||||
|
/// <param name="colorStops">An array of color stops sorted by their position.</param>
|
||||
|
/// <param name="repetitionMode">Defines if and how the gradient should be repeated.</param>
|
||||
|
protected GradientBrushApplicatorBase( |
||||
|
ImageFrame<TPixel> target, |
||||
|
GraphicsOptions options, |
||||
|
ColorStop<TPixel>[] colorStops, |
||||
|
GradientRepetitionMode repetitionMode) |
||||
|
: base(target, options) |
||||
|
{ |
||||
|
this.colorStops = colorStops; // TODO: requires colorStops to be sorted by position - should that be checked?
|
||||
|
this.repetitionMode = repetitionMode; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Base implementation of the indexer for gradients
|
||||
|
/// (follows the facade pattern, using abstract methods)
|
||||
|
/// </summary>
|
||||
|
/// <param name="x">X coordinate of the Pixel.</param>
|
||||
|
/// <param name="y">Y coordinate of the Pixel.</param>
|
||||
|
internal override TPixel this[int x, int y] |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
float positionOnCompleteGradient = this.PositionOnGradient(x, y); |
||||
|
|
||||
|
switch (this.repetitionMode) |
||||
|
{ |
||||
|
case GradientRepetitionMode.None: |
||||
|
// do nothing. The following could be done, but is not necessary:
|
||||
|
// onLocalGradient = Math.Min(0, Math.Max(1, onLocalGradient));
|
||||
|
break; |
||||
|
case GradientRepetitionMode.Repeat: |
||||
|
positionOnCompleteGradient = positionOnCompleteGradient % 1; |
||||
|
break; |
||||
|
case GradientRepetitionMode.Reflect: |
||||
|
positionOnCompleteGradient = positionOnCompleteGradient % 2; |
||||
|
if (positionOnCompleteGradient > 1) |
||||
|
{ |
||||
|
positionOnCompleteGradient = 2 - positionOnCompleteGradient; |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
case GradientRepetitionMode.DontFill: |
||||
|
if (positionOnCompleteGradient > 1 || positionOnCompleteGradient < 0) |
||||
|
{ |
||||
|
return NamedColors<TPixel>.Transparent; |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
default: |
||||
|
throw new ArgumentOutOfRangeException(); |
||||
|
} |
||||
|
|
||||
|
var (from, to) = this.GetGradientSegment(positionOnCompleteGradient); |
||||
|
|
||||
|
if (from.Color.Equals(to.Color)) |
||||
|
{ |
||||
|
return from.Color; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var fromAsVector = from.Color.ToVector4(); |
||||
|
var toAsVector = to.Color.ToVector4(); |
||||
|
float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / to.Ratio; |
||||
|
|
||||
|
// TODO: this should be changeble for different gradienting functions
|
||||
|
Vector4 result = PorterDuffFunctions.Normal( |
||||
|
fromAsVector, |
||||
|
toAsVector, |
||||
|
onLocalGradient); |
||||
|
|
||||
|
TPixel resultColor = default; |
||||
|
resultColor.PackFromVector4(result); |
||||
|
return resultColor; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// calculates the position on the gradient for a given pixel.
|
||||
|
/// This method is abstract as it's content depends on the shape of the gradient.
|
||||
|
/// </summary>
|
||||
|
/// <param name="x">The x coordinate of the pixel</param>
|
||||
|
/// <param name="y">The y coordinate of the pixel</param>
|
||||
|
/// <returns>
|
||||
|
/// The position the given pixel 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 <see cref="GradientRepetitionMode" /> enum.
|
||||
|
/// </returns>
|
||||
|
protected abstract float PositionOnGradient(int x, int y); |
||||
|
|
||||
|
private (ColorStop<TPixel> from, ColorStop<TPixel> to) GetGradientSegment( |
||||
|
float positionOnCompleteGradient) |
||||
|
{ |
||||
|
var localGradientFrom = this.colorStops[0]; |
||||
|
ColorStop<TPixel> 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 > positionOnCompleteGradient) |
||||
|
{ |
||||
|
// we're done here, so break it!
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
localGradientFrom = localGradientTo; |
||||
|
} |
||||
|
|
||||
|
return (localGradientFrom, localGradientTo); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Modes to repeat a gradient.
|
||||
|
/// </summary>
|
||||
|
public enum GradientRepetitionMode |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// don't repeat, keep the color of start and end beyond those points stable.
|
||||
|
/// </summary>
|
||||
|
None, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Repeat the gradient.
|
||||
|
/// If it's a black-white gradient, with Repeat it will be Black->{gray}->White|Black->{gray}->White|...
|
||||
|
/// </summary>
|
||||
|
Repeat, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reflect the gradient.
|
||||
|
/// Similar to <see cref="Repeat"/>, but each other repetition uses inverse order of <see cref="ColorStop{TPixel}"/>s.
|
||||
|
/// Used on a Black-White gradient, Reflect leads to Black->{gray}->White->{gray}->White...
|
||||
|
/// </summary>
|
||||
|
Reflect, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// With DontFill a gradient does not touch any pixel beyond it's borders.
|
||||
|
/// For the <see cref="LinearGradientBrush{TPixel}" /> this is beyond the orthogonal through start and end,
|
||||
|
/// TODO For the cref="PolygonalGradientBrush" it's outside the polygon,
|
||||
|
/// For <see cref="RadialGradientBrush{TPixel}" /> and <see cref="EllipticGradientBrush{TPixel}" /> it's beyond 1.0.
|
||||
|
/// </summary>
|
||||
|
DontFill |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,152 @@ |
|||||
|
using System; |
||||
|
|
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Provides an implementation of a brush for painting linear gradients within areas.
|
||||
|
/// Supported right now:
|
||||
|
/// - a set of colors in relative distances to each other.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format</typeparam>
|
||||
|
public sealed class LinearGradientBrush<TPixel> : GradientBrushBase<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
private readonly Point p1; |
||||
|
|
||||
|
private readonly Point p2; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="LinearGradientBrush{TPixel}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="p1">Start point</param>
|
||||
|
/// <param name="p2">End point</param>
|
||||
|
/// <param name="repetitionMode">defines how colors are repeated.</param>
|
||||
|
/// <param name="colorStops"><inheritdoc /></param>
|
||||
|
public LinearGradientBrush( |
||||
|
Point p1, |
||||
|
Point p2, |
||||
|
GradientRepetitionMode repetitionMode, |
||||
|
params ColorStop<TPixel>[] colorStops) |
||||
|
: base(repetitionMode, colorStops) |
||||
|
{ |
||||
|
this.p1 = p1; |
||||
|
this.p2 = p2; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override BrushApplicator<TPixel> CreateApplicator(ImageFrame<TPixel> source, RectangleF region, GraphicsOptions options) |
||||
|
=> new LinearGradientBrushApplicator(source, this.p1, this.p2, this.ColorStops, this.RepetitionMode, options); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The linear gradient brush applicator.
|
||||
|
/// </summary>
|
||||
|
private sealed class LinearGradientBrushApplicator : GradientBrushApplicatorBase |
||||
|
{ |
||||
|
private readonly Point start; |
||||
|
|
||||
|
private readonly Point end; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// the vector along the gradient, x component
|
||||
|
/// </summary>
|
||||
|
private readonly float alongX; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// the vector along the gradient, y component
|
||||
|
/// </summary>
|
||||
|
private readonly float alongY; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// the vector perpendicular to the gradient, y component
|
||||
|
/// </summary>
|
||||
|
private readonly float acrossY; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// the vector perpendicular to the gradient, x component
|
||||
|
/// </summary>
|
||||
|
private readonly float acrossX; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// the result of <see cref="alongX"/>^2 + <see cref="alongY"/>^2
|
||||
|
/// </summary>
|
||||
|
private readonly float alongsSquared; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// the length of the defined gradient (between source and end)
|
||||
|
/// </summary>
|
||||
|
private readonly float length; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="LinearGradientBrushApplicator" /> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The source</param>
|
||||
|
/// <param name="start">start point of the gradient</param>
|
||||
|
/// <param name="end">end point of the gradient</param>
|
||||
|
/// <param name="colorStops">tuple list of colors and their respective position between 0 and 1 on the line</param>
|
||||
|
/// <param name="repetitionMode">defines how the gradient colors are repeated.</param>
|
||||
|
/// <param name="options">the graphics options</param>
|
||||
|
public LinearGradientBrushApplicator( |
||||
|
ImageFrame<TPixel> source, |
||||
|
Point start, |
||||
|
Point end, |
||||
|
ColorStop<TPixel>[] colorStops, |
||||
|
GradientRepetitionMode repetitionMode, |
||||
|
GraphicsOptions options) |
||||
|
: base(source, options, colorStops, repetitionMode) |
||||
|
{ |
||||
|
this.start = start; |
||||
|
this.end = end; |
||||
|
|
||||
|
// 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); |
||||
|
} |
||||
|
|
||||
|
protected override float PositionOnGradient(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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override void Dispose() |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,102 @@ |
|||||
|
using System; |
||||
|
|
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A Circular Gradient Brush, defined by center point and radius.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
public sealed class RadialGradientBrush<TPixel> : GradientBrushBase<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
private readonly Point center; |
||||
|
|
||||
|
private readonly float radius; |
||||
|
|
||||
|
/// <inheritdoc cref="GradientBrushBase{TPixel}" />
|
||||
|
/// <param name="center">The center of the circular gradient and 0 for the color stops.</param>
|
||||
|
/// <param name="radius">The radius of the circular gradient and 1 for the color stops.</param>
|
||||
|
/// <param name="repetitionMode">Defines how the colors in the gradient are repeated.</param>
|
||||
|
/// <param name="colorStops">the color stops as defined in base class.</param>
|
||||
|
public RadialGradientBrush( |
||||
|
Point center, |
||||
|
float radius, |
||||
|
GradientRepetitionMode repetitionMode, |
||||
|
params ColorStop<TPixel>[] colorStops) |
||||
|
: base(repetitionMode, colorStops) |
||||
|
{ |
||||
|
this.center = center; |
||||
|
this.radius = radius; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="CreateApplicator" />
|
||||
|
public override BrushApplicator<TPixel> CreateApplicator( |
||||
|
ImageFrame<TPixel> source, |
||||
|
RectangleF region, |
||||
|
GraphicsOptions options) => |
||||
|
new RadialGradientBrushApplicator( |
||||
|
source, |
||||
|
options, |
||||
|
this.center, |
||||
|
this.radius, |
||||
|
this.ColorStops, |
||||
|
this.RepetitionMode); |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
private sealed class RadialGradientBrushApplicator : GradientBrushApplicatorBase |
||||
|
{ |
||||
|
private readonly Point center; |
||||
|
|
||||
|
private readonly float radius; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="RadialGradientBrushApplicator" /> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="target">The target image</param>
|
||||
|
/// <param name="options">The options.</param>
|
||||
|
/// <param name="center">Center point of the gradient.</param>
|
||||
|
/// <param name="radius">Radius of the gradient.</param>
|
||||
|
/// <param name="colorStops">Definition of colors.</param>
|
||||
|
/// <param name="repetitionMode">How the colors are repeated beyond the first gradient.</param>
|
||||
|
public RadialGradientBrushApplicator( |
||||
|
ImageFrame<TPixel> target, |
||||
|
GraphicsOptions options, |
||||
|
Point center, |
||||
|
float radius, |
||||
|
ColorStop<TPixel>[] colorStops, |
||||
|
GradientRepetitionMode repetitionMode) |
||||
|
: base(target, options, colorStops, repetitionMode) |
||||
|
{ |
||||
|
this.center = center; |
||||
|
this.radius = radius; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="Dispose" />
|
||||
|
public override void Dispose() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// As this is a circular gradient, the position on the gradient is based on
|
||||
|
/// the distance of the point to the center.
|
||||
|
/// </summary>
|
||||
|
/// <param name="x">The X coordinate of the target pixel.</param>
|
||||
|
/// <param name="y">The Y coordinate of the target pixel.</param>
|
||||
|
/// <returns>the position on the color gradient.</returns>
|
||||
|
protected override float PositionOnGradient(int x, int y) |
||||
|
{ |
||||
|
float distance = (float)Math.Sqrt(Math.Pow(this.center.X - x, 2) + Math.Pow(this.center.Y - y, 2)); |
||||
|
return distance / this.radius; |
||||
|
} |
||||
|
|
||||
|
internal override void Apply(Span<float> scanline, int x, int y) |
||||
|
{ |
||||
|
// TODO: each row is symmetric across center, so we can calculate half of it and mirror it to improve performance.
|
||||
|
base.Apply(scanline, x, y); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,149 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
using SixLabors.ImageSharp.Processing.Drawing; |
||||
|
using SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes; |
||||
|
|
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Drawing |
||||
|
{ |
||||
|
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
||||
|
|
||||
|
[GroupOutput("Drawing/GradientBrushes")] |
||||
|
public class FillEllipticGradientBrushTests |
||||
|
{ |
||||
|
public static ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(10, 10, PixelTypes.Rgba32)] |
||||
|
public void WithEqualColorsReturnsUnicolorImage<TPixel>( |
||||
|
TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
TPixel red = NamedColors<TPixel>.Red; |
||||
|
|
||||
|
using (Image<TPixel> image = provider.GetImage()) |
||||
|
{ |
||||
|
var unicolorLinearGradientBrush = |
||||
|
new EllipticGradientBrush<TPixel>( |
||||
|
new SixLabors.Primitives.Point(0, 0), |
||||
|
new SixLabors.Primitives.Point(10, 0), |
||||
|
1.0f, |
||||
|
GradientRepetitionMode.None, |
||||
|
new ColorStop<TPixel>(0, red), |
||||
|
new ColorStop<TPixel>(1, red)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
|
||||
|
image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); |
||||
|
|
||||
|
// no need for reference image in this test:
|
||||
|
image.ComparePixelBufferTo(red); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.1)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.4)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.8)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 1.0)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 1.2)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 1.6)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 2.0)] |
||||
|
public void AxisParallelEllipsesWithDifferentRatio<TPixel>( |
||||
|
TestImageProvider<TPixel> provider, |
||||
|
float ratio) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
TPixel yellow = NamedColors<TPixel>.Yellow; |
||||
|
TPixel red = NamedColors<TPixel>.Red; |
||||
|
TPixel black = NamedColors<TPixel>.Black; |
||||
|
|
||||
|
provider.VerifyOperation( |
||||
|
TolerantComparer, |
||||
|
image => |
||||
|
{ |
||||
|
var unicolorLinearGradientBrush = new EllipticGradientBrush<TPixel>( |
||||
|
new SixLabors.Primitives.Point(image.Width / 2, image.Height / 2), |
||||
|
new SixLabors.Primitives.Point(image.Width / 2, (image.Width * 2) / 3), |
||||
|
ratio, |
||||
|
GradientRepetitionMode.None, |
||||
|
new ColorStop<TPixel>(0, yellow), |
||||
|
new ColorStop<TPixel>(1, red), |
||||
|
new ColorStop<TPixel>(1, black)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
}, |
||||
|
$"{ratio:F2}", |
||||
|
false, |
||||
|
false); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.1, 0)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.4, 0)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.8, 0)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 1.0, 0)] |
||||
|
|
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.1, 45)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.4, 45)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.8, 45)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 1.0, 45)] |
||||
|
|
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.1, 90)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.4, 90)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.8, 90)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 1.0, 90)] |
||||
|
|
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.1, 30)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.4, 30)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0.8, 30)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 1.0, 30)] |
||||
|
public void RotatedEllipsesWithDifferentRatio<TPixel>( |
||||
|
TestImageProvider<TPixel> provider, |
||||
|
float ratio, |
||||
|
float rotationInDegree) |
||||
|
where TPixel: struct, IPixel<TPixel> |
||||
|
{ |
||||
|
FormattableString variant = $"{ratio:F2}_AT_{rotationInDegree:00}deg"; |
||||
|
|
||||
|
provider.VerifyOperation( |
||||
|
TolerantComparer, |
||||
|
image => |
||||
|
{ |
||||
|
TPixel yellow = NamedColors<TPixel>.Yellow; |
||||
|
TPixel red = NamedColors<TPixel>.Red; |
||||
|
TPixel black = NamedColors<TPixel>.Black; |
||||
|
|
||||
|
var center = new SixLabors.Primitives.Point(image.Width / 2, image.Height / 2); |
||||
|
|
||||
|
double rotation = (Math.PI * rotationInDegree) / 180.0; |
||||
|
double cos = Math.Cos(rotation); |
||||
|
double sin = Math.Sin(rotation); |
||||
|
|
||||
|
int offsetY = image.Height / 6; |
||||
|
int axisX = center.X + (int)-(offsetY * sin); |
||||
|
int axisY = center.Y + (int)(offsetY * cos); |
||||
|
|
||||
|
var unicolorLinearGradientBrush = new EllipticGradientBrush<TPixel>( |
||||
|
center, |
||||
|
new SixLabors.Primitives.Point(axisX, axisY), |
||||
|
ratio, |
||||
|
GradientRepetitionMode.None, |
||||
|
new ColorStop<TPixel>(0, yellow), |
||||
|
new ColorStop<TPixel>(1, red), |
||||
|
new ColorStop<TPixel>(1, black)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
}, |
||||
|
variant, |
||||
|
false, |
||||
|
false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,351 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using System.Linq; |
||||
|
using System.Text; |
||||
|
|
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
using SixLabors.ImageSharp.Processing.Drawing; |
||||
|
using SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes; |
||||
|
|
||||
|
using Xunit; |
||||
|
// ReSharper disable InconsistentNaming
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Drawing |
||||
|
{ |
||||
|
using SixLabors.ImageSharp.Advanced; |
||||
|
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
||||
|
|
||||
|
[GroupOutput("Drawing/GradientBrushes")] |
||||
|
public class FillLinearGradientBrushTests |
||||
|
{ |
||||
|
public static ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(10, 10, PixelTypes.Rgba32)] |
||||
|
public void WithEqualColorsReturnsUnicolorImage<TPixel>(TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
using (Image<TPixel> image = provider.GetImage()) |
||||
|
{ |
||||
|
TPixel red = NamedColors<TPixel>.Red; |
||||
|
|
||||
|
var unicolorLinearGradientBrush = new LinearGradientBrush<TPixel>( |
||||
|
new SixLabors.Primitives.Point(0, 0), |
||||
|
new SixLabors.Primitives.Point(10, 0), |
||||
|
GradientRepetitionMode.None, |
||||
|
new ColorStop<TPixel>(0, red), |
||||
|
new ColorStop<TPixel>(1, red)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
|
||||
|
image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); |
||||
|
|
||||
|
// no need for reference image in this test:
|
||||
|
image.ComparePixelBufferTo(red); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(20, 10, PixelTypes.Rgba32)] |
||||
|
[WithBlankImages(20, 10, PixelTypes.Argb32)] |
||||
|
[WithBlankImages(20, 10, PixelTypes.Rgb24)] |
||||
|
public void DoesNotDependOnSinglePixelType<TPixel>(TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
provider.VerifyOperation( |
||||
|
TolerantComparer, |
||||
|
image => |
||||
|
{ |
||||
|
var unicolorLinearGradientBrush = new LinearGradientBrush<TPixel>( |
||||
|
new SixLabors.Primitives.Point(0, 0), |
||||
|
new SixLabors.Primitives.Point(image.Width, 0), |
||||
|
GradientRepetitionMode.None, |
||||
|
new ColorStop<TPixel>(0, NamedColors<TPixel>.Blue), |
||||
|
new ColorStop<TPixel>(1, NamedColors<TPixel>.Yellow)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
}, |
||||
|
appendSourceFileOrDescription: false); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(500, 10, PixelTypes.Rgba32)] |
||||
|
public void HorizontalReturnsUnicolorColumns<TPixel>(TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
provider.VerifyOperation( |
||||
|
TolerantComparer, |
||||
|
image => |
||||
|
{ |
||||
|
TPixel red = NamedColors<TPixel>.Red; |
||||
|
TPixel yellow = NamedColors<TPixel>.Yellow; |
||||
|
|
||||
|
var unicolorLinearGradientBrush = new LinearGradientBrush<TPixel>( |
||||
|
new SixLabors.Primitives.Point(0, 0), |
||||
|
new SixLabors.Primitives.Point(image.Width, 0), |
||||
|
GradientRepetitionMode.None, |
||||
|
new ColorStop<TPixel>(0, red), |
||||
|
new ColorStop<TPixel>(1, yellow)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
}, |
||||
|
false, |
||||
|
false); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.DontFill)] |
||||
|
[WithBlankImages(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.None)] |
||||
|
[WithBlankImages(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.Repeat)] |
||||
|
[WithBlankImages(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.Reflect)] |
||||
|
public void HorizontalGradientWithRepMode<TPixel>( |
||||
|
TestImageProvider<TPixel> provider, |
||||
|
GradientRepetitionMode repetitionMode) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
provider.VerifyOperation( |
||||
|
TolerantComparer, |
||||
|
image => |
||||
|
{ |
||||
|
TPixel red = NamedColors<TPixel>.Red; |
||||
|
TPixel yellow = NamedColors<TPixel>.Yellow; |
||||
|
|
||||
|
var unicolorLinearGradientBrush = new LinearGradientBrush<TPixel>( |
||||
|
new SixLabors.Primitives.Point(0, 0), |
||||
|
new SixLabors.Primitives.Point(image.Width / 10, 0), |
||||
|
repetitionMode, |
||||
|
new ColorStop<TPixel>(0, red), |
||||
|
new ColorStop<TPixel>(1, yellow)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
}, |
||||
|
$"{repetitionMode}", |
||||
|
false, |
||||
|
false); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(200, 100, PixelTypes.Rgba32, new[] { 0.5f })] |
||||
|
[WithBlankImages(200, 100, PixelTypes.Rgba32, new[] { 0.2f, 0.4f, 0.6f, 0.8f })] |
||||
|
[WithBlankImages(200, 100, PixelTypes.Rgba32, new[] { 0.1f, 0.3f, 0.6f })] |
||||
|
public void WithDoubledStopsProduceDashedPatterns<TPixel>( |
||||
|
TestImageProvider<TPixel> provider, |
||||
|
float[] pattern) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
string variant = string.Join("_", pattern.Select(i => i.ToString(CultureInfo.InvariantCulture))); |
||||
|
|
||||
|
// ensure the input data is valid
|
||||
|
Assert.True(pattern.Length > 0); |
||||
|
|
||||
|
TPixel black = NamedColors<TPixel>.Black; |
||||
|
TPixel white = NamedColors<TPixel>.White; |
||||
|
|
||||
|
// create the input pattern: 0, followed by each of the arguments twice, followed by 1.0 - toggling black and white.
|
||||
|
ColorStop<TPixel>[] colorStops = |
||||
|
Enumerable.Repeat(new ColorStop<TPixel>(0, black), 1) |
||||
|
.Concat( |
||||
|
pattern |
||||
|
.SelectMany((f, index) => new[] |
||||
|
{ |
||||
|
new ColorStop<TPixel>(f, index % 2 == 0 ? black : white), |
||||
|
new ColorStop<TPixel>(f, index % 2 == 0 ? white : black) |
||||
|
})) |
||||
|
.Concat(Enumerable.Repeat(new ColorStop<TPixel>(1, pattern.Length % 2 == 0 ? black : white), 1)) |
||||
|
.ToArray(); |
||||
|
|
||||
|
using (Image<TPixel> image = provider.GetImage()) |
||||
|
{ |
||||
|
var unicolorLinearGradientBrush = |
||||
|
new LinearGradientBrush<TPixel>( |
||||
|
new SixLabors.Primitives.Point(0, 0), |
||||
|
new SixLabors.Primitives.Point(image.Width, 0), |
||||
|
GradientRepetitionMode.None, |
||||
|
colorStops); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
|
||||
|
image.DebugSave( |
||||
|
provider, |
||||
|
variant, |
||||
|
appendPixelTypeToFileName: false, |
||||
|
appendSourceFileOrDescription: false); |
||||
|
|
||||
|
// the result must be a black and white pattern, no other color should occur:
|
||||
|
Assert.All( |
||||
|
Enumerable.Range(0, image.Width).Select(i => image[i, 0]), |
||||
|
color => Assert.True(color.Equals(black) || color.Equals(white))); |
||||
|
|
||||
|
image.CompareToReferenceOutput( |
||||
|
TolerantComparer, |
||||
|
provider, |
||||
|
variant, |
||||
|
appendPixelTypeToFileName: false, |
||||
|
appendSourceFileOrDescription: false); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(10, 500, PixelTypes.Rgba32)] |
||||
|
public void VerticalBrushReturnsUnicolorRows<TPixel>( |
||||
|
TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
provider.VerifyOperation( |
||||
|
image => |
||||
|
{ |
||||
|
TPixel red = NamedColors<TPixel>.Red; |
||||
|
TPixel yellow = NamedColors<TPixel>.Yellow; |
||||
|
|
||||
|
var unicolorLinearGradientBrush = new LinearGradientBrush<TPixel>( |
||||
|
new SixLabors.Primitives.Point(0, 0), |
||||
|
new SixLabors.Primitives.Point(0, image.Height), |
||||
|
GradientRepetitionMode.None, |
||||
|
new ColorStop<TPixel>(0, red), |
||||
|
new ColorStop<TPixel>(1, yellow)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
|
||||
|
VerifyAllRowsAreUnicolor(image); |
||||
|
}, |
||||
|
false, |
||||
|
false); |
||||
|
|
||||
|
void VerifyAllRowsAreUnicolor(Image<TPixel> image) |
||||
|
{ |
||||
|
for (int y = 0; y < image.Height; y++) |
||||
|
{ |
||||
|
Span<TPixel> row = image.GetPixelRowSpan(y); |
||||
|
TPixel firstColorOfRow = row[0]; |
||||
|
foreach (TPixel p in row) |
||||
|
{ |
||||
|
Assert.Equal(firstColorOfRow, p); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public enum ImageCorner |
||||
|
{ |
||||
|
TopLeft = 0, |
||||
|
TopRight = 1, |
||||
|
BottomLeft = 2, |
||||
|
BottomRight = 3 |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, ImageCorner.TopLeft)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, ImageCorner.TopRight)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, ImageCorner.BottomLeft)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, ImageCorner.BottomRight)] |
||||
|
public void DiagonalReturnsCorrectImages<TPixel>( |
||||
|
TestImageProvider<TPixel> provider, |
||||
|
ImageCorner startCorner) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
using (Image<TPixel> image = provider.GetImage()) |
||||
|
{ |
||||
|
Assert.True(image.Height == image.Width, "For the math check block at the end the image must be squared, but it is not."); |
||||
|
|
||||
|
int startX = (int)startCorner % 2 == 0 ? 0 : image.Width - 1; |
||||
|
int startY = startCorner > ImageCorner.TopRight ? 0 : image.Height - 1; |
||||
|
int endX = image.Height - startX - 1; |
||||
|
int endY = image.Width - startY - 1; |
||||
|
|
||||
|
TPixel red = NamedColors<TPixel>.Red; |
||||
|
TPixel yellow = NamedColors<TPixel>.Yellow; |
||||
|
|
||||
|
var unicolorLinearGradientBrush = |
||||
|
new LinearGradientBrush<TPixel>( |
||||
|
new SixLabors.Primitives.Point(startX, startY), |
||||
|
new SixLabors.Primitives.Point(endX, endY), |
||||
|
GradientRepetitionMode.None, |
||||
|
new ColorStop<TPixel>(0, red), |
||||
|
new ColorStop<TPixel>(1, yellow)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
image.DebugSave( |
||||
|
provider, |
||||
|
startCorner, |
||||
|
appendPixelTypeToFileName: false, |
||||
|
appendSourceFileOrDescription: false); |
||||
|
|
||||
|
int verticalSign = startY == 0 ? 1 : -1; |
||||
|
int horizontalSign = startX == 0 ? 1 : -1; |
||||
|
|
||||
|
// check first and last pixel, these are known:
|
||||
|
Assert.Equal(red, image[startX, startY]); |
||||
|
Assert.Equal(yellow, image[endX, endY]); |
||||
|
|
||||
|
for (int i = 0; i < image.Height; i++) |
||||
|
{ |
||||
|
// it's diagonal, so for any (a, a) on the gradient line, for all (a-x, b+x) - +/- depending on the diagonal direction - must be the same color)
|
||||
|
TPixel colorOnDiagonal = image[i, i]; |
||||
|
int orthoCount = 0; |
||||
|
for (int offset = -orthoCount; offset < orthoCount; offset++) |
||||
|
{ |
||||
|
Assert.Equal(colorOnDiagonal, image[i + horizontalSign * offset, i + verticalSign * offset]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
image.CompareToReferenceOutput( |
||||
|
TolerantComparer, |
||||
|
provider, |
||||
|
startCorner, |
||||
|
appendPixelTypeToFileName: false, |
||||
|
appendSourceFileOrDescription: false); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(500, 500, PixelTypes.Rgba32, 0, 0, 499, 499, new[] { 0f, .2f, .5f, .9f }, new[] { 0, 0, 1, 1 })] |
||||
|
[WithBlankImages(500, 500, PixelTypes.Rgba32, 0, 499, 499, 0, new[] { 0f, 0.2f, 0.5f, 0.9f }, new[] { 0, 1, 2, 3 })] |
||||
|
[WithBlankImages(500, 500, PixelTypes.Rgba32, 499, 499, 0, 0, new[] { 0f, 0.7f, 0.8f, 0.9f}, new[] { 0, 1, 2, 0 })] |
||||
|
[WithBlankImages(500, 500, PixelTypes.Rgba32, 0, 0, 499, 499, new[] { 0f, .5f, 1f}, new[]{0, 1, 3})] |
||||
|
public void ArbitraryGradients<TPixel>( |
||||
|
TestImageProvider<TPixel> provider, |
||||
|
int startX, int startY, |
||||
|
int endX, int endY, |
||||
|
float[] stopPositions, |
||||
|
int[] stopColorCodes) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
TPixel[] colors = { |
||||
|
NamedColors<TPixel>.Navy, NamedColors<TPixel>.LightGreen, NamedColors<TPixel>.Yellow, |
||||
|
NamedColors<TPixel>.Red |
||||
|
}; |
||||
|
|
||||
|
var coloringVariant = new StringBuilder(); |
||||
|
ColorStop<TPixel>[] colorStops = new ColorStop<TPixel>[stopPositions.Length]; |
||||
|
for (int i = 0; i < stopPositions.Length; i++) |
||||
|
{ |
||||
|
TPixel color = colors[stopColorCodes[i % colors.Length]]; |
||||
|
float position = stopPositions[i]; |
||||
|
|
||||
|
colorStops[i] = new ColorStop<TPixel>(position, color); |
||||
|
coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", color, position); |
||||
|
} |
||||
|
|
||||
|
FormattableString variant = $"({startX},{startY})_TO_({endX},{endY})__[{coloringVariant}]"; |
||||
|
|
||||
|
provider.VerifyOperation( |
||||
|
image => |
||||
|
{ |
||||
|
var unicolorLinearGradientBrush = new LinearGradientBrush<TPixel>( |
||||
|
new SixLabors.Primitives.Point(startX, startY), |
||||
|
new SixLabors.Primitives.Point(endX, endY), |
||||
|
GradientRepetitionMode.None, |
||||
|
colorStops); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
}, |
||||
|
variant, |
||||
|
false, |
||||
|
false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
using SixLabors.ImageSharp.Processing.Drawing; |
||||
|
using SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes; |
||||
|
|
||||
|
using Xunit; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Drawing |
||||
|
{ |
||||
|
using System; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
||||
|
|
||||
|
[GroupOutput("Drawing/GradientBrushes")] |
||||
|
public class FillRadialGradientBrushTests |
||||
|
{ |
||||
|
public static ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32)] |
||||
|
public void WithEqualColorsReturnsUnicolorImage<TPixel>( |
||||
|
TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
using (Image<TPixel> image = provider.GetImage()) |
||||
|
{ |
||||
|
TPixel red = NamedColors<TPixel>.Red; |
||||
|
|
||||
|
var unicolorRadialGradientBrush = |
||||
|
new RadialGradientBrush<TPixel>( |
||||
|
new SixLabors.Primitives.Point(0, 0), |
||||
|
100, |
||||
|
GradientRepetitionMode.None, |
||||
|
new ColorStop<TPixel>(0, red), |
||||
|
new ColorStop<TPixel>(1, red)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorRadialGradientBrush)); |
||||
|
|
||||
|
image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); |
||||
|
|
||||
|
// no need for reference image in this test:
|
||||
|
image.ComparePixelBufferTo(red); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 100, 100)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0, 0)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 100, 0)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, 0, 100)] |
||||
|
[WithBlankImages(200, 200, PixelTypes.Rgba32, -40, 100)] |
||||
|
public void WithDifferentCentersReturnsImage<TPixel>( |
||||
|
TestImageProvider<TPixel> provider, |
||||
|
int centerX, |
||||
|
int centerY) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
provider.VerifyOperation( |
||||
|
TolerantComparer, |
||||
|
image => |
||||
|
{ |
||||
|
var brush = new RadialGradientBrush<TPixel>( |
||||
|
new SixLabors.Primitives.Point(centerX, centerY), |
||||
|
image.Width / 2f, |
||||
|
GradientRepetitionMode.None, |
||||
|
new ColorStop<TPixel>(0, NamedColors<TPixel>.Red), |
||||
|
new ColorStop<TPixel>(1, NamedColors<TPixel>.Yellow)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(brush)); |
||||
|
}, |
||||
|
$"center({centerX},{centerY})", |
||||
|
false, |
||||
|
false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1 +1 @@ |
|||||
Subproject commit f641620eb5378db49d6153bbf1443ad13bda2379 |
Subproject commit 8ab54f8003aff94b3a9662b0be46b0062cad6b74 |
||||
Loading…
Reference in new issue