mirror of https://github.com/SixLabors/ImageSharp
2 changed files with 267 additions and 0 deletions
@ -0,0 +1,143 @@ |
|||||
|
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 class EllipticGradientBrush<TPixel> : AbstractGradientBrush<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
private readonly Point center; |
||||
|
|
||||
|
private readonly Point referenceAxisEnd; |
||||
|
|
||||
|
private readonly float axisRatio; |
||||
|
|
||||
|
/// <inheritdoc cref="AbstractGradientBrush{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="colorStops">the color stops as defined in base class.</param>
|
||||
|
public EllipticGradientBrush( |
||||
|
Point center, |
||||
|
Point referenceAxisEnd, |
||||
|
float axisRatio, |
||||
|
params ColorStop<TPixel>[] colorStops) |
||||
|
: base(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, |
||||
|
region); |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected class RadialGradientBrushApplicator : AbstractGradientBrushApplicator |
||||
|
{ |
||||
|
private readonly Point center; |
||||
|
|
||||
|
private readonly Point referenceAxisEnd; |
||||
|
|
||||
|
private readonly float axisRatio; |
||||
|
|
||||
|
private readonly double rotation; |
||||
|
|
||||
|
private readonly float referenceRadius; |
||||
|
|
||||
|
private readonly float secondRadius; |
||||
|
|
||||
|
/// <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="region">TODO !</param>
|
||||
|
public RadialGradientBrushApplicator( |
||||
|
ImageFrame<TPixel> target, |
||||
|
GraphicsOptions options, |
||||
|
Point center, |
||||
|
Point referenceAxisEnd, |
||||
|
float axisRatio, |
||||
|
ColorStop<TPixel>[] colorStops, |
||||
|
RectangleF region) |
||||
|
: base(target, options, colorStops, region) |
||||
|
{ |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override void Dispose() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override float PositionOnGradient(int xt, int yt) |
||||
|
{ |
||||
|
float x0 = xt - this.center.X; // TODO: rotate this point after translation
|
||||
|
float y0 = yt - this.center.Y; |
||||
|
|
||||
|
float x = (float)((x0 * Math.Cos(this.rotation)) - (y0 * Math.Sin(this.rotation))); // TODO: store sin and cos of rotation as constant!
|
||||
|
float y = (float)((x0 * Math.Sin(this.rotation)) + (y0 * Math.Cos(this.rotation))); |
||||
|
|
||||
|
var inBoundaryChecker = ((x * x) / (this.referenceRadius * this.referenceRadius)) |
||||
|
+ ((y * y) / (this.secondRadius * this.secondRadius)); |
||||
|
|
||||
|
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) |
||||
|
{ |
||||
|
return (float)Math.Sqrt(Math.Pow(p1.X - p2.X, 2) + Math.Pow(p1.Y - p2.Y, 2)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,124 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
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 |
||||
|
{ |
||||
|
public class FillEllipticGradientBrushTests : FileTestBase |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void EllipticGradientBrushWithEqualColorsAndReturnsUnicolorImage() |
||||
|
{ |
||||
|
string path = TestEnvironment.CreateOutputDirectory("Fill", "EllipticGradientBrush"); |
||||
|
using (var image = new Image<Rgba32>(10, 10)) |
||||
|
{ |
||||
|
EllipticGradientBrush<Rgba32> unicolorLinearGradientBrush = |
||||
|
new EllipticGradientBrush<Rgba32>( |
||||
|
new SixLabors.Primitives.Point(0, 0), |
||||
|
new SixLabors.Primitives.Point(10, 0), |
||||
|
1.0f, |
||||
|
new ColorStop<Rgba32>(0, Rgba32.Red), |
||||
|
new ColorStop<Rgba32>(1, Rgba32.Red)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
image.Save($"{path}/UnicolorCircleGradient.png"); |
||||
|
|
||||
|
using (PixelAccessor<Rgba32> sourcePixels = image.Lock()) |
||||
|
{ |
||||
|
Assert.Equal(Rgba32.Red, sourcePixels[0, 0]); |
||||
|
Assert.Equal(Rgba32.Red, sourcePixels[9, 9]); |
||||
|
Assert.Equal(Rgba32.Red, sourcePixels[5, 5]); |
||||
|
Assert.Equal(Rgba32.Red, sourcePixels[3, 8]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0.1)] |
||||
|
[InlineData(0.4)] |
||||
|
[InlineData(0.8)] |
||||
|
[InlineData(1.0)] |
||||
|
[InlineData(1.2)] |
||||
|
[InlineData(1.6)] |
||||
|
[InlineData(2.0)] |
||||
|
public void EllipticGradientBrushProducesAxisParallelEllipsesWithDifferentRatio( |
||||
|
float ratio) |
||||
|
{ |
||||
|
string path = TestEnvironment.CreateOutputDirectory("Fill", "EllipticGradientBrush"); |
||||
|
using (var image = new Image<Rgba32>(1000, 1000)) |
||||
|
{ |
||||
|
EllipticGradientBrush<Rgba32> unicolorLinearGradientBrush = |
||||
|
new EllipticGradientBrush<Rgba32>( |
||||
|
new SixLabors.Primitives.Point(500, 500), |
||||
|
new SixLabors.Primitives.Point(500, 750), |
||||
|
ratio, |
||||
|
new ColorStop<Rgba32>(0, Rgba32.Yellow), |
||||
|
new ColorStop<Rgba32>(1, Rgba32.Red), |
||||
|
new ColorStop<Rgba32>(1, Rgba32.Black)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
image.Save($"{path}/Ellipsis{ratio}.png"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0.1, 0)] |
||||
|
[InlineData(0.4, 0)] |
||||
|
[InlineData(0.8, 0)] |
||||
|
[InlineData(1.0, 0)] |
||||
|
|
||||
|
[InlineData(0.1, 45)] |
||||
|
[InlineData(0.4, 45)] |
||||
|
[InlineData(0.8, 45)] |
||||
|
[InlineData(1.0, 45)] |
||||
|
|
||||
|
[InlineData(0.1, 90)] |
||||
|
[InlineData(0.4, 90)] |
||||
|
[InlineData(0.8, 90)] |
||||
|
[InlineData(1.0, 90)] |
||||
|
|
||||
|
[InlineData(0.1, 30)] |
||||
|
[InlineData(0.4, 30)] |
||||
|
[InlineData(0.8, 30)] |
||||
|
[InlineData(1.0, 30)] |
||||
|
public void EllipticGradientBrushProducesRotatedEllipsesWithDifferentRatio( |
||||
|
float ratio, |
||||
|
float rotationInDegree) |
||||
|
{ |
||||
|
var center = new SixLabors.Primitives.Point(500, 500); |
||||
|
|
||||
|
var rotation = (Math.PI * rotationInDegree) / 180.0; |
||||
|
var cos = Math.Cos(rotation); |
||||
|
var sin = Math.Sin(rotation); |
||||
|
|
||||
|
int axisX = (int)((center.X * cos) - (center.Y * sin)); |
||||
|
int axisY = (int)((center.X * sin) + (center.Y * cos)); |
||||
|
|
||||
|
string path = TestEnvironment.CreateOutputDirectory("Fill", "EllipticGradientBrush"); |
||||
|
using (var image = new Image<Rgba32>(1000, 1000)) |
||||
|
{ |
||||
|
EllipticGradientBrush<Rgba32> unicolorLinearGradientBrush = |
||||
|
new EllipticGradientBrush<Rgba32>( |
||||
|
center, |
||||
|
new SixLabors.Primitives.Point(axisX, axisY), |
||||
|
ratio, |
||||
|
new ColorStop<Rgba32>(0, Rgba32.Yellow), |
||||
|
new ColorStop<Rgba32>(1, Rgba32.Red), |
||||
|
new ColorStop<Rgba32>(1, Rgba32.Black)); |
||||
|
|
||||
|
image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); |
||||
|
image.Save($"{path}/Ellipsis{ratio}_rot{rotationInDegree}°.png"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue