Browse Source

#542: refactor to prepare for other gradients

- move to GradientBrushes namespace
- add abstract base class for Gradient Brushes, that implements the GetGradientSegment implementtion and the color picking.
af/merge-core
Unknown 8 years ago
parent
commit
6a371908d6
  1. 132
      src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/AbstractGradientBrush{TPixel}.cs
  2. 36
      src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/ColorStop.cs
  3. 130
      src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/LinearGradientBrush{TPixel}.cs
  4. 37
      tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs

132
src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/AbstractGradientBrush{TPixel}.cs

@ -0,0 +1,132 @@
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 AbstractGradientBrush<TPixel> : IBrush<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <inheritdoc cref="IBrush{TPixel}"/>
/// <param name="colorStops">The gradient colors.</param>
protected AbstractGradientBrush(params ColorStop<TPixel>[] colorStops)
{
this.ColorStops = colorStops;
}
/// <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 AbstractGradientBrushApplicator : BrushApplicator<TPixel>
{
private readonly ColorStop<TPixel>[] colorStops;
/// <summary>
/// Initializes a new instance of the <see cref="AbstractGradientBrushApplicator"/> 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="region">TODO: use region, compare with other Brushes for reference</param>
protected AbstractGradientBrushApplicator(
ImageFrame<TPixel> target,
GraphicsOptions options,
ColorStop<TPixel>[] colorStops,
RectangleF region)
: base(target, options)
{
this.colorStops = colorStops; // TODO: requires colorStops to be sorted by position - should that be checked?
}
/// <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);
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);
}
}
}
}

36
src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/ColorStop.cs

@ -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; }
}
}

130
src/ImageSharp.Drawing/Processing/Drawing/Brushes/LinearGradientBrush{TPixel}.cs → src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/LinearGradientBrush{TPixel}.cs

@ -1,89 +1,49 @@
using System;
using System.Diagnostics;
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.PixelFormats.PixelBlenders;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Drawing.Brushes
namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes
{
/// <summary>
/// Provides an implementation of a brush for painting gradients within areas.
/// 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.
/// - two points to gradient along.
/// </summary>
/// <typeparam name="TPixel">The pixel format</typeparam>
public class LinearGradientBrush<TPixel> : IBrush<TPixel>
public class LinearGradientBrush<TPixel> : AbstractGradientBrush<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly Point p1;
private readonly Point p2;
private readonly ColorStop[] colorStops;
/// <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="colorStops">
/// A set of color keys and where they are.
/// The double should be in range [0..1] and is relative between p1 and p2.
/// TODO: what about the [0..1] restriction? is it necessary? If so, it should be checked, if not, it should be explained what happens for greater/smaller values.
/// </param>
public LinearGradientBrush(Point p1, Point p2, params ColorStop[] colorStops)
/// <param name="colorStops"><inheritdoc /></param>
public LinearGradientBrush(Point p1, Point p2, params ColorStop<TPixel>[] colorStops)
: base(colorStops)
{
this.p1 = p1;
this.p2 = p2;
this.colorStops = colorStops;
}
/// <inheritdoc />
public BrushApplicator<TPixel> CreateApplicator(ImageFrame<TPixel> source, RectangleF region, GraphicsOptions options)
=> new LinearGradientBrushApplicator(source, this.p1, this.p2, this.colorStops, region, options);
/// <summary>
/// A struct that defines a single color stop.
/// </summary>
[DebuggerDisplay("ColorStop({Ratio} -> {Color}")]
public struct ColorStop
{
/// <summary>
/// Initializes a new instance of the <see cref="ColorStop" /> struct.
/// </summary>
/// <param name="ratio">Where should it be? 0 is at the start, 1 at the end of the <see cref="LinearGradientBrush{TPixel}"/>.</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 <see cref="LinearGradientBrush{TPixel}" /> gradient axis.
/// </summary>
public float Ratio { get; }
/// <summary>
/// Gets the color to be used.
/// </summary>
public TPixel Color { get; }
}
public override BrushApplicator<TPixel> CreateApplicator(ImageFrame<TPixel> source, RectangleF region, GraphicsOptions options)
=> new LinearGradientBrushApplicator(source, this.p1, this.p2, this.ColorStops, region, options);
/// <summary>
/// The linear gradient brush applicator.
/// </summary>
private class LinearGradientBrushApplicator : BrushApplicator<TPixel>
private class LinearGradientBrushApplicator : AbstractGradientBrushApplicator
{
private readonly Point start;
private readonly Point end;
private readonly ColorStop[] colorStops;
/// <summary>
/// the vector along the gradient, x component
/// </summary>
@ -127,14 +87,13 @@ namespace SixLabors.ImageSharp.Processing.Drawing.Brushes
ImageFrame<TPixel> source,
Point start,
Point end,
ColorStop[] colorStops,
RectangleF region, // TODO: use region, compare with other Brushes for reference.
ColorStop<TPixel>[] colorStops,
RectangleF region,
GraphicsOptions options)
: base(source, options)
: base(source, options, colorStops, region)
{
this.start = start;
this.end = end;
this.colorStops = colorStops; // TODO: requires colorStops to be sorted by Item1!
// the along vector:
this.alongX = this.end.X - this.start.X;
@ -149,61 +108,7 @@ namespace SixLabors.ImageSharp.Processing.Drawing.Brushes
this.length = (float)Math.Sqrt(this.alongsSquared);
}
/// <summary>
/// Gets the color for a single pixel
/// </summary>
/// <param name="x">The x coordinate.</param>
/// <param name="y">The y coordinate.</param>
internal override TPixel this[int x, int y]
{
get
{
// the following formula is the result of the linear equation system that forms the vector.
// TODO: this formula should be abstracted as it's the only difference between linear and radial gradient!
float onCompleteGradient = this.RatioOnGradient(x, y);
var localGradientFrom = this.colorStops[0];
ColorStop 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 > onCompleteGradient)
{
// we're done here, so break it!
break;
}
localGradientFrom = localGradientTo;
}
TPixel resultColor = default;
if (localGradientFrom.Color.Equals(localGradientTo.Color))
{
resultColor = localGradientFrom.Color;
}
else
{
var fromAsVector = localGradientFrom.Color.ToVector4();
var toAsVector = localGradientTo.Color.ToVector4();
float onLocalGradient = (onCompleteGradient - localGradientFrom.Ratio) / localGradientTo.Ratio; // TODO:
Vector4 result = PorterDuffFunctions.Normal(
fromAsVector,
toAsVector,
onLocalGradient);
// TODO: when resultColor is a struct, what does PackFromVector4 do here?
resultColor.PackFromVector4(result);
}
return resultColor;
}
}
private float RatioOnGradient(int x, int y)
protected override float PositionOnGradient(int x, int y)
{
if (this.acrossX == 0)
{
@ -234,6 +139,10 @@ namespace SixLabors.ImageSharp.Processing.Drawing.Brushes
}
}
public override void Dispose()
{
}
internal override void Apply(Span<float> scanline, int x, int y)
{
base.Apply(scanline, x, y);
@ -257,11 +166,6 @@ namespace SixLabors.ImageSharp.Processing.Drawing.Brushes
// this.Blender.Blend(memoryManager, destinationRow, destinationRow, this.Colors.Span, amountSpan);
// }
}
/// <inheritdoc />
public override void Dispose()
{
}
}
}
}

37
tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs

@ -2,11 +2,12 @@
// Licensed under the Apache License, Version 2.0.
using System.Linq;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Drawing.Brushes;
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
@ -23,8 +24,8 @@ namespace SixLabors.ImageSharp.Tests.Drawing
new LinearGradientBrush<Rgba32>(
new SixLabors.Primitives.Point(0, 0),
new SixLabors.Primitives.Point(10, 0),
new LinearGradientBrush<Rgba32>.ColorStop(0, Rgba32.Red),
new LinearGradientBrush<Rgba32>.ColorStop(1, Rgba32.Red));
new ColorStop<Rgba32>(0, Rgba32.Red),
new ColorStop<Rgba32>(1, Rgba32.Red));
image.Mutate(x => x.Fill(unicolorLinearGradientBrush));
image.Save($"{path}/UnicolorGradient.png");
@ -53,8 +54,8 @@ namespace SixLabors.ImageSharp.Tests.Drawing
new LinearGradientBrush<Rgba32>(
new SixLabors.Primitives.Point(0, 0),
new SixLabors.Primitives.Point(500, 0),
new LinearGradientBrush<Rgba32>.ColorStop(0, Rgba32.Red),
new LinearGradientBrush<Rgba32>.ColorStop(1, Rgba32.Yellow));
new ColorStop<Rgba32>(0, Rgba32.Red),
new ColorStop<Rgba32>(1, Rgba32.Yellow));
image.Mutate(x => x.Fill(unicolorLinearGradientBrush));
image.Save($"{path}/horizontalRedToYellow.png");
@ -97,16 +98,16 @@ namespace SixLabors.ImageSharp.Tests.Drawing
Assert.True(pattern.Length > 0);
// create the input pattern: 0, followed by each of the arguments twice, followed by 1.0 - toggling black and white.
LinearGradientBrush<Rgba32>.ColorStop[] colorStops =
Enumerable.Repeat(new LinearGradientBrush<Rgba32>.ColorStop(0, Rgba32.Black), 1)
ColorStop<Rgba32>[] colorStops =
Enumerable.Repeat(new ColorStop<Rgba32>(0, Rgba32.Black), 1)
.Concat(
pattern
.SelectMany((f, index) => new[]
{
new LinearGradientBrush<Rgba32>.ColorStop(f, index % 2 == 0 ? Rgba32.Black : Rgba32.White),
new LinearGradientBrush<Rgba32>.ColorStop(f, index % 2 == 0 ? Rgba32.White : Rgba32.Black)
new ColorStop<Rgba32>(f, index % 2 == 0 ? Rgba32.Black : Rgba32.White),
new ColorStop<Rgba32>(f, index % 2 == 0 ? Rgba32.White : Rgba32.Black)
}))
.Concat(Enumerable.Repeat(new LinearGradientBrush<Rgba32>.ColorStop(1, pattern.Length % 2 == 0 ? Rgba32.Black : Rgba32.White), 1))
.Concat(Enumerable.Repeat(new ColorStop<Rgba32>(1, pattern.Length % 2 == 0 ? Rgba32.Black : Rgba32.White), 1))
.ToArray();
string path = TestEnvironment.CreateOutputDirectory("Fill", "LinearGradientBrush");
@ -145,8 +146,8 @@ namespace SixLabors.ImageSharp.Tests.Drawing
new LinearGradientBrush<Rgba32>(
new SixLabors.Primitives.Point(0, 0),
new SixLabors.Primitives.Point(0, 500),
new LinearGradientBrush<Rgba32>.ColorStop(0, Rgba32.Red),
new LinearGradientBrush<Rgba32>.ColorStop(1, Rgba32.Yellow));
new ColorStop<Rgba32>(0, Rgba32.Red),
new ColorStop<Rgba32>(1, Rgba32.Yellow));
image.Mutate(x => x.Fill(unicolorLinearGradientBrush));
image.Save($"{path}/verticalRedToYellow.png");
@ -154,7 +155,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing
using (PixelAccessor<Rgba32> sourcePixels = image.Lock())
{
Rgba32 firstRowColor = sourcePixels[0, 0];
Rgba32 columnColor23 = sourcePixels[0, 23];
Rgba32 columnColor42 = sourcePixels[0, 42];
Rgba32 columnColor333 = sourcePixels[0, 333];
@ -193,8 +194,8 @@ namespace SixLabors.ImageSharp.Tests.Drawing
new LinearGradientBrush<Rgba32>(
new SixLabors.Primitives.Point(startX, startY),
new SixLabors.Primitives.Point(endX, endY),
new LinearGradientBrush<Rgba32>.ColorStop(0, Rgba32.Red),
new LinearGradientBrush<Rgba32>.ColorStop(1, Rgba32.Yellow));
new ColorStop<Rgba32>(0, Rgba32.Red),
new ColorStop<Rgba32>(1, Rgba32.Yellow));
image.Mutate(x => x.Fill(unicolorLinearGradientBrush));
image.Save($"{path}/diagonalRedToYellowFrom{startX}_{startY}.png");
@ -233,10 +234,10 @@ namespace SixLabors.ImageSharp.Tests.Drawing
Rgba32.Red
};
var colorStops = new LinearGradientBrush<Rgba32>.ColorStop[stopPositions.Length];
var colorStops = new ColorStop<Rgba32>[stopPositions.Length];
for (int i = 0; i < stopPositions.Length; i++)
{
colorStops[i] = new LinearGradientBrush<Rgba32>.ColorStop(
colorStops[i] = new ColorStop<Rgba32>(
stopPositions[i],
colors[stopColorCodes[i]]);
}

Loading…
Cancel
Save