Browse Source

Merge pull request #71 from JimBobSquarePants/optimise-polygon-fill

Optimise polygon fill
pull/73/head
Scott Williams 10 years ago
committed by GitHub
parent
commit
61afe82571
  1. 4
      src/ImageSharp.Drawing/Brushes/IBrush.cs
  2. 8
      src/ImageSharp.Drawing/Brushes/ImageBrush{TColor}.cs
  3. 8
      src/ImageSharp.Drawing/Brushes/PatternBrush{TColor}.cs
  4. 11
      src/ImageSharp.Drawing/Brushes/Processors/BrushApplicator.cs
  5. 9
      src/ImageSharp.Drawing/Brushes/RecolorBrush{TColor}.cs
  6. 8
      src/ImageSharp.Drawing/Brushes/SolidBrush{TColor}.cs
  7. 152
      src/ImageSharp.Drawing/Paths/InternalPath.cs
  8. 2
      src/ImageSharp.Drawing/Pens/IPen.cs
  9. 22
      src/ImageSharp.Drawing/Pens/Pen{TColor}.cs
  10. 13
      src/ImageSharp.Drawing/Pens/Processors/PenApplicator.cs
  11. 2
      src/ImageSharp.Drawing/Processors/DrawPathProcessor.cs
  12. 4
      src/ImageSharp.Drawing/Processors/FillProcessor.cs
  13. 419
      src/ImageSharp.Drawing/Processors/FillShapeProcessor.cs
  14. 25
      src/ImageSharp.Drawing/Shapes/BezierPolygon.cs
  15. 70
      src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs
  16. 22
      src/ImageSharp.Drawing/Shapes/IShape.cs
  17. 31
      src/ImageSharp.Drawing/Shapes/LinearPolygon.cs
  18. 27
      src/ImageSharp.Drawing/Shapes/Polygon.cs
  19. 46
      src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs
  20. 6
      tests/ImageSharp.Tests/Drawing/SolidComplexPolygonTests.cs
  21. 2
      tests/ImageSharp.Tests/Drawing/SolidPolygonTests.cs

4
src/ImageSharp.Drawing/Brushes/IBrush.cs

@ -14,7 +14,7 @@ namespace ImageSharp.Drawing
/// </summary>
/// <typeparam name="TColor">The pixel format.</typeparam>
/// <remarks>
/// A brush is a simple class that will return an <see cref="IBrushApplicator{TColor}" /> that will perform the
/// A brush is a simple class that will return an <see cref="BrushApplicator{TColor}" /> that will perform the
/// logic for converting a pixel location to a <typeparamref name="TColor"/>.
/// </remarks>
public interface IBrush<TColor>
@ -32,6 +32,6 @@ namespace ImageSharp.Drawing
/// The <paramref name="region" /> when being applied to things like shapes would usually be the
/// bounding box of the shape not necessarily the bounds of the whole image
/// </remarks>
IBrushApplicator<TColor> CreateApplicator(PixelAccessor<TColor> pixelSource, RectangleF region);
BrushApplicator<TColor> CreateApplicator(PixelAccessor<TColor> pixelSource, RectangleF region);
}
}

8
src/ImageSharp.Drawing/Brushes/ImageBrush{TColor}.cs

@ -32,7 +32,7 @@ namespace ImageSharp.Drawing.Brushes
}
/// <inheritdoc />
public IBrushApplicator<TColor> CreateApplicator(PixelAccessor<TColor> sourcePixels, RectangleF region)
public BrushApplicator<TColor> CreateApplicator(PixelAccessor<TColor> sourcePixels, RectangleF region)
{
return new ImageBrushApplicator(this.image, region);
}
@ -40,7 +40,7 @@ namespace ImageSharp.Drawing.Brushes
/// <summary>
/// The image brush applicator.
/// </summary>
private class ImageBrushApplicator : IBrushApplicator<TColor>
private class ImageBrushApplicator : BrushApplicator<TColor>
{
/// <summary>
/// The source pixel accessor.
@ -86,7 +86,7 @@ namespace ImageSharp.Drawing.Brushes
/// <returns>
/// The color
/// </returns>
public TColor GetColor(Vector2 point)
public override TColor GetColor(Vector2 point)
{
// Offset the requested pixel by the value in the rectangle (the shapes position)
point = point - this.offset;
@ -97,7 +97,7 @@ namespace ImageSharp.Drawing.Brushes
}
/// <inheritdoc />
public void Dispose()
public override void Dispose()
{
this.source.Dispose();
}

8
src/ImageSharp.Drawing/Brushes/PatternBrush{TColor}.cs

@ -95,7 +95,7 @@ namespace ImageSharp.Drawing.Brushes
}
/// <inheritdoc />
public IBrushApplicator<TColor> CreateApplicator(PixelAccessor<TColor> sourcePixels, RectangleF region)
public BrushApplicator<TColor> CreateApplicator(PixelAccessor<TColor> sourcePixels, RectangleF region)
{
return new PatternBrushApplicator(this.pattern, this.stride);
}
@ -103,7 +103,7 @@ namespace ImageSharp.Drawing.Brushes
/// <summary>
/// The pattern brush applicator.
/// </summary>
private class PatternBrushApplicator : IBrushApplicator<TColor>
private class PatternBrushApplicator : BrushApplicator<TColor>
{
/// <summary>
/// The patter x-length.
@ -139,7 +139,7 @@ namespace ImageSharp.Drawing.Brushes
/// <returns>
/// The color
/// </returns>
public TColor GetColor(Vector2 point)
public override TColor GetColor(Vector2 point)
{
int x = (int)point.X % this.xLength;
int y = (int)point.Y % this.stride;
@ -148,7 +148,7 @@ namespace ImageSharp.Drawing.Brushes
}
/// <inheritdoc />
public void Dispose()
public override void Dispose()
{
// noop
}

11
src/ImageSharp.Drawing/Brushes/Processors/IBrushApplicator.cs → src/ImageSharp.Drawing/Brushes/Processors/BrushApplicator.cs

@ -1,4 +1,4 @@
// <copyright file="IBrushApplicator.cs" company="James Jackson-South">
// <copyright file="BrushApplicator.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
@ -13,14 +13,19 @@ namespace ImageSharp.Drawing.Processors
/// </summary>
/// <typeparam name="TColor">The pixel format.</typeparam>
/// <seealso cref="System.IDisposable" />
public interface IBrushApplicator<TColor> : IDisposable // disposable will be required if/when there is an ImageBrush
public abstract class BrushApplicator<TColor> : IDisposable // disposable will be required if/when there is an ImageBrush
where TColor : struct, IPackedPixel, IEquatable<TColor>
{
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public abstract void Dispose();
/// <summary>
/// Gets the color for a single pixel.
/// </summary>
/// <param name="point">The point.</param>
/// <returns>The color</returns>
TColor GetColor(Vector2 point);
public abstract TColor GetColor(Vector2 point);
}
}

9
src/ImageSharp.Drawing/Brushes/RecolorBrush{TColor}.cs

@ -55,7 +55,7 @@ namespace ImageSharp.Drawing.Brushes
public TColor TargetColor { get; }
/// <inheritdoc />
public IBrushApplicator<TColor> CreateApplicator(PixelAccessor<TColor> sourcePixels, RectangleF region)
public BrushApplicator<TColor> CreateApplicator(PixelAccessor<TColor> sourcePixels, RectangleF region)
{
return new RecolorBrushApplicator(sourcePixels, this.SourceColor, this.TargetColor, this.Threshold);
}
@ -63,7 +63,7 @@ namespace ImageSharp.Drawing.Brushes
/// <summary>
/// The recolor brush applicator.
/// </summary>
private class RecolorBrushApplicator : IBrushApplicator<TColor>
private class RecolorBrushApplicator : BrushApplicator<TColor>
{
/// <summary>
/// The source pixel accessor.
@ -113,7 +113,7 @@ namespace ImageSharp.Drawing.Brushes
/// <returns>
/// The color
/// </returns>
public TColor GetColor(Vector2 point)
public override TColor GetColor(Vector2 point)
{
// Offset the requested pixel by the value in the rectangle (the shapes position)
TColor result = this.source[(int)point.X, (int)point.Y];
@ -130,9 +130,8 @@ namespace ImageSharp.Drawing.Brushes
}
/// <inheritdoc />
public void Dispose()
public override void Dispose()
{
// we didn't make the lock on the PixelAccessor we shouldn't release it.
}
}
}

8
src/ImageSharp.Drawing/Brushes/SolidBrush{TColor}.cs

@ -40,7 +40,7 @@ namespace ImageSharp.Drawing.Brushes
public TColor Color => this.color;
/// <inheritdoc />
public IBrushApplicator<TColor> CreateApplicator(PixelAccessor<TColor> sourcePixels, RectangleF region)
public BrushApplicator<TColor> CreateApplicator(PixelAccessor<TColor> sourcePixels, RectangleF region)
{
return new SolidBrushApplicator(this.color);
}
@ -48,7 +48,7 @@ namespace ImageSharp.Drawing.Brushes
/// <summary>
/// The solid brush applicator.
/// </summary>
private class SolidBrushApplicator : IBrushApplicator<TColor>
private class SolidBrushApplicator : BrushApplicator<TColor>
{
/// <summary>
/// The solid color.
@ -71,13 +71,13 @@ namespace ImageSharp.Drawing.Brushes
/// <returns>
/// The color
/// </returns>
public TColor GetColor(Vector2 point)
public override TColor GetColor(Vector2 point)
{
return this.color;
}
/// <inheritdoc />
public void Dispose()
public override void Dispose()
{
// noop
}

152
src/ImageSharp.Drawing/Paths/InternalPath.cs

@ -14,6 +14,11 @@ namespace ImageSharp.Drawing.Paths
/// </summary>
internal class InternalPath
{
/// <summary>
/// The maximum vector
/// </summary>
private static readonly Vector2 MaxVector = new Vector2(float.MaxValue);
/// <summary>
/// The locker.
/// </summary>
@ -164,7 +169,47 @@ namespace ImageSharp.Drawing.Paths
}
/// <summary>
/// Points the in polygon.
/// Based on a line described by <paramref name="start" /> and <paramref name="end" />
/// populate a buffer for all points on the path that the line intersects.
/// </summary>
/// <param name="start">The start.</param>
/// <param name="end">The end.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="count">The count.</param>
/// <param name="offset">The offset.</param>
/// <returns>number iof intersections hit</returns>
public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset)
{
int polyCorners = this.points.Length;
if (!this.closedPath)
{
polyCorners -= 1;
}
int position = 0;
for (int i = 0; i < polyCorners && count > 0; i++)
{
int next = i + 1;
if (this.closedPath && next == polyCorners)
{
next = 0;
}
Vector2 point = FindIntersection(this.points[i], this.points[next], start, end);
if (point != MaxVector)
{
buffer[position + offset] = point;
position++;
count--;
}
}
return position;
}
/// <summary>
/// Determines if the specified point is inside or outside the path.
/// </summary>
/// <param name="point">The point.</param>
/// <returns>Returns true if the point is inside the closed path.</returns>
@ -203,6 +248,111 @@ namespace ImageSharp.Drawing.Paths
return oddNodes;
}
/// <summary>
/// Determins if the bounding box for 2 lines
/// described by <paramref name="line1Start" /> and <paramref name="line1End" />
/// and <paramref name="line2Start" /> and <paramref name="line2End" /> overlap.
/// </summary>
/// <param name="line1Start">The line1 start.</param>
/// <param name="line1End">The line1 end.</param>
/// <param name="line2Start">The line2 start.</param>
/// <param name="line2End">The line2 end.</param>
/// <returns>Returns true it the bounding box of the 2 lines intersect</returns>
private static bool BoundingBoxesIntersect(Vector2 line1Start, Vector2 line1End, Vector2 line2Start, Vector2 line2End)
{
Vector2 topLeft1 = Vector2.Min(line1Start, line1End);
Vector2 bottomRight1 = Vector2.Max(line1Start, line1End);
Vector2 topLeft2 = Vector2.Min(line2Start, line2End);
Vector2 bottomRight2 = Vector2.Max(line2Start, line2End);
float left1 = topLeft1.X;
float right1 = bottomRight1.X;
float top1 = topLeft1.Y;
float bottom1 = bottomRight1.Y;
float left2 = topLeft2.X;
float right2 = bottomRight2.X;
float top2 = topLeft2.Y;
float bottom2 = bottomRight2.Y;
return left1 <= right2 && right1 >= left2
&&
top1 <= bottom2 && bottom1 >= top2;
}
/// <summary>
/// Finds the point on line described by <paramref name="line1Start" /> and <paramref name="line1End" />
/// that intersects with line described by <paramref name="line2Start" /> and <paramref name="line2End" />
/// </summary>
/// <param name="line1Start">The line1 start.</param>
/// <param name="line1End">The line1 end.</param>
/// <param name="line2Start">The line2 start.</param>
/// <param name="line2End">The line2 end.</param>
/// <returns>
/// A <see cref="Vector2"/> describing the point that the 2 lines cross or <see cref="MaxVector"/> if they do not.
/// </returns>
private static Vector2 FindIntersection(Vector2 line1Start, Vector2 line1End, Vector2 line2Start, Vector2 line2End)
{
// do bounding boxes overlap, if not then the lines can't and return fast.
if (!BoundingBoxesIntersect(line1Start, line1End, line2Start, line2End))
{
return MaxVector;
}
Vector2 line1Diff = line1End - line1Start;
Vector2 line2Diff = line2End - line2Start;
Vector2 point;
if (line1Diff.X == 0)
{
float slope = line2Diff.Y / line2Diff.X;
float yinter = line2Start.Y - (slope * line2Start.X);
float y = (line1Start.X * slope) + yinter;
point = new Vector2(line1Start.X, y);
// horizontal and vertical lines
}
else if (line2Diff.X == 0)
{
float slope = line1Diff.Y / line1Diff.X;
float yinter = line1Start.Y - (slope * line1Start.X);
float y = (line2Start.X * slope) + yinter;
point = new Vector2(line2Start.X, y);
// horizontal and vertical lines
}
else
{
float slope1 = line1Diff.Y / line1Diff.X;
float slope2 = line2Diff.Y / line2Diff.X;
float yinter1 = line1Start.Y - (slope1 * line1Start.X);
float yinter2 = line2Start.Y - (slope2 * line2Start.X);
if (slope1 == slope2 && yinter1 != yinter2)
{
return MaxVector;
}
float x = (yinter2 - yinter1) / (slope1 - slope2);
float y = (slope1 * x) + yinter1;
point = new Vector2(x, y);
}
if (BoundingBoxesIntersect(line1Start, line1End, point, point))
{
return point;
}
else if (BoundingBoxesIntersect(line2Start, line2End, point, point))
{
return point;
}
return MaxVector;
}
/// <summary>
/// Simplifies the collection of segments.
/// </summary>

2
src/ImageSharp.Drawing/Pens/IPen.cs

@ -26,6 +26,6 @@ namespace ImageSharp.Drawing.Pens
/// <remarks>
/// The <paramref name="region" /> when being applied to things like shapes would usually be the bounding box of the shape not necessarily the shape of the whole image.
/// </remarks>
IPenApplicator<TColor> CreateApplicator(PixelAccessor<TColor> pixelSource, RectangleF region);
PenApplicator<TColor> CreateApplicator(PixelAccessor<TColor> pixelSource, RectangleF region);
}
}

22
src/ImageSharp.Drawing/Pens/Pen{TColor}.cs

@ -111,7 +111,7 @@ namespace ImageSharp.Drawing.Pens
/// The <paramref name="region" /> when being applied to things like shapes would ussually be the
/// bounding box of the shape not necorserrally the shape of the whole image
/// </remarks>
public IPenApplicator<TColor> CreateApplicator(PixelAccessor<TColor> sourcePixels, RectangleF region)
public PenApplicator<TColor> CreateApplicator(PixelAccessor<TColor> sourcePixels, RectangleF region)
{
if (this.pattern == null || this.pattern.Length < 2)
{
@ -123,9 +123,9 @@ namespace ImageSharp.Drawing.Pens
return new PatternPenApplicator(sourcePixels, this.Brush, region, this.Width, this.pattern);
}
private class SolidPenApplicator : IPenApplicator<TColor>
private class SolidPenApplicator : PenApplicator<TColor>
{
private readonly IBrushApplicator<TColor> brush;
private readonly BrushApplicator<TColor> brush;
private readonly float halfWidth;
public SolidPenApplicator(PixelAccessor<TColor> sourcePixels, IBrush<TColor> brush, RectangleF region, float width)
@ -135,17 +135,17 @@ namespace ImageSharp.Drawing.Pens
this.RequiredRegion = RectangleF.Outset(region, width);
}
public RectangleF RequiredRegion
public override RectangleF RequiredRegion
{
get;
}
public void Dispose()
public override void Dispose()
{
this.brush.Dispose();
}
public ColoredPointInfo<TColor> GetColor(PointInfo info)
public override ColoredPointInfo<TColor> GetColor(PointInfo info)
{
var result = default(ColoredPointInfo<TColor>);
result.Color = this.brush.GetColor(info.SearchPoint);
@ -164,9 +164,9 @@ namespace ImageSharp.Drawing.Pens
}
}
private class PatternPenApplicator : IPenApplicator<TColor>
private class PatternPenApplicator : PenApplicator<TColor>
{
private readonly IBrushApplicator<TColor> brush;
private readonly BrushApplicator<TColor> brush;
private readonly float halfWidth;
private readonly float[] pattern;
private readonly float totalLength;
@ -188,17 +188,17 @@ namespace ImageSharp.Drawing.Pens
this.RequiredRegion = RectangleF.Outset(region, width);
}
public RectangleF RequiredRegion
public override RectangleF RequiredRegion
{
get;
}
public void Dispose()
public override void Dispose()
{
this.brush.Dispose();
}
public ColoredPointInfo<TColor> GetColor(PointInfo info)
public override ColoredPointInfo<TColor> GetColor(PointInfo info)
{
var infoResult = default(ColoredPointInfo<TColor>);
infoResult.DistanceFromElement = float.MaxValue; // is really outside the element

13
src/ImageSharp.Drawing/Pens/Processors/IPenApplicator.cs → src/ImageSharp.Drawing/Pens/Processors/PenApplicator.cs

@ -1,4 +1,4 @@
// <copyright file="IPenApplicator.cs" company="James Jackson-South">
// <copyright file="PenApplicator.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
@ -12,7 +12,7 @@ namespace ImageSharp.Drawing.Processors
/// primitive that converts a <see cref="PointInfo"/> into a color and a distance away from the drawable part of the path.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
public interface IPenApplicator<TColor> : IDisposable
public abstract class PenApplicator<TColor> : IDisposable
where TColor : struct, IPackedPixel, IEquatable<TColor>
{
/// <summary>
@ -21,13 +21,18 @@ namespace ImageSharp.Drawing.Processors
/// <value>
/// The required region.
/// </value>
RectangleF RequiredRegion { get; }
public abstract RectangleF RequiredRegion { get; }
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public abstract void Dispose();
/// <summary>
/// Gets a <see cref="ColoredPointInfo{TColor}" /> from a point represented by a <see cref="PointInfo" />.
/// </summary>
/// <param name="info">The information to extract color details about.</param>
/// <returns>Returns the color details and distance from a solid bit of the line.</returns>
ColoredPointInfo<TColor> GetColor(PointInfo info);
public abstract ColoredPointInfo<TColor> GetColor(PointInfo info);
}
}

2
src/ImageSharp.Drawing/Processors/DrawPathProcessor.cs

@ -84,7 +84,7 @@ namespace ImageSharp.Drawing.Processors
protected override void OnApply(ImageBase<TColor> source, Rectangle sourceRectangle)
{
using (PixelAccessor<TColor> sourcePixels = source.Lock())
using (IPenApplicator<TColor> applicator = this.pen.CreateApplicator(sourcePixels, this.region))
using (PenApplicator<TColor> applicator = this.pen.CreateApplicator(sourcePixels, this.region))
{
var rect = RectangleF.Ceiling(applicator.RequiredRegion);

4
src/ImageSharp.Drawing/Processors/FillProcessor.cs

@ -62,7 +62,7 @@ namespace ImageSharp.Drawing.Processors
// for example If brush is SolidBrush<TColor> then we could just get the color upfront
// and skip using the IBrushApplicator<TColor>?.
using (PixelAccessor<TColor> sourcePixels = source.Lock())
using (IBrushApplicator<TColor> applicator = this.brush.CreateApplicator(sourcePixels, sourceRectangle))
using (BrushApplicator<TColor> applicator = this.brush.CreateApplicator(sourcePixels, sourceRectangle))
{
Parallel.For(
minY,
@ -83,7 +83,7 @@ namespace ImageSharp.Drawing.Processors
Vector4 backgroundVector = sourcePixels[offsetX, offsetY].ToVector4();
Vector4 sourceVector = applicator.GetColor(currentPoint).ToVector4();
var finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, 1);
Vector4 finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, 1);
TColor packed = default(TColor);
packed.PackFromVector4(finalColor);

419
src/ImageSharp.Drawing/Processors/FillShapeProcessor.cs

@ -6,6 +6,7 @@
namespace ImageSharp.Drawing.Processors
{
using System;
using System.Buffers;
using System.Numerics;
using System.Threading.Tasks;
using Drawing;
@ -32,7 +33,7 @@ namespace ImageSharp.Drawing.Processors
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="shape">The shape.</param>
/// <param name="options">The graphics options.</param>
/// <param name="options">The options.</param>
public FillShapeProcessor(IBrush<TColor> brush, IShape shape, GraphicsOptions options)
{
this.poly = shape;
@ -61,19 +62,12 @@ namespace ImageSharp.Drawing.Processors
minY = Math.Max(0, minY);
maxY = Math.Min(source.Height, maxY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
ArrayPool<Vector2> arrayPool = ArrayPool<Vector2>.Shared;
if (minY > 0)
{
polyStartY = 0;
}
int maxIntersections = this.poly.MaxIntersections;
using (PixelAccessor<TColor> sourcePixels = source.Lock())
using (IBrushApplicator<TColor> applicator = this.fillColor.CreateApplicator(sourcePixels, rect))
using (BrushApplicator<TColor> applicator = this.fillColor.CreateApplicator(sourcePixels, rect))
{
Parallel.For(
minY,
@ -81,57 +75,396 @@ namespace ImageSharp.Drawing.Processors
this.ParallelOptions,
y =>
{
int offsetY = y - polyStartY;
Vector2 currentPoint = default(Vector2);
Vector2[] buffer = arrayPool.Rent(maxIntersections);
for (int x = minX; x < maxX; x++)
try
{
int offsetX = x - startX;
currentPoint.X = offsetX;
currentPoint.Y = offsetY;
float dist = this.poly.Distance(currentPoint);
float opacity = this.Opacity(dist);
Vector2 left = new Vector2(startX, y);
Vector2 right = new Vector2(endX, y);
// foreach line we get all the points where this line crosses the polygon
int pointsFound = this.poly.FindIntersections(left, right, buffer, maxIntersections, 0);
if (pointsFound == 0)
{
arrayPool.Return(buffer);
// nothign on this line skip
return;
}
QuickSortX(buffer, pointsFound);
if (opacity > Constants.Epsilon)
int currentIntersection = 0;
float nextPoint = buffer[0].X;
float lastPoint = float.MinValue;
bool isInside = false;
// every odd point is the start of a line
Vector2 currentPoint = default(Vector2);
for (int x = minX; x < maxX; x++)
{
Vector4 backgroundVector = sourcePixels[offsetX, offsetY].ToVector4();
Vector4 sourceVector = applicator.GetColor(currentPoint).ToVector4();
currentPoint.X = x;
currentPoint.Y = y;
if (!isInside)
{
if (x < (nextPoint - DrawPadding) && x > (lastPoint + DrawPadding))
{
if (nextPoint == right.X)
{
// we are in the ends run skip it
x = maxX;
continue;
}
// lets just jump forward
x = (int)Math.Floor(nextPoint) - DrawPadding;
}
}
bool onCorner = false;
// there seems to be some issue with this switch.
if (x >= nextPoint)
{
currentIntersection++;
lastPoint = nextPoint;
if (currentIntersection == pointsFound)
{
nextPoint = right.X;
}
else
{
nextPoint = buffer[currentIntersection].X;
Vector4 finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity);
finalColor.W = backgroundVector.W;
// double point from a corner flip the bit back and move on again
if (nextPoint == lastPoint)
{
onCorner = true;
isInside ^= true;
currentIntersection++;
if (currentIntersection == pointsFound)
{
nextPoint = right.X;
}
else
{
nextPoint = buffer[currentIntersection].X;
}
}
}
TColor packed = default(TColor);
packed.PackFromVector4(finalColor);
sourcePixels[offsetX, offsetY] = packed;
isInside ^= true;
}
float opacity = 1;
if (!isInside && !onCorner)
{
if (this.options.Antialias)
{
float distance = float.MaxValue;
if (x == lastPoint || x == nextPoint)
{
// we are to far away from the line
distance = 0;
}
else if (nextPoint - AntialiasFactor < x)
{
// we are near the left of the line
distance = nextPoint - x;
}
else if (lastPoint + AntialiasFactor > x)
{
// we are near the right of the line
distance = x - lastPoint;
}
else
{
// we are to far away from the line
continue;
}
opacity = 1 - (distance / AntialiasFactor);
}
else
{
continue;
}
}
if (opacity > Constants.Epsilon)
{
Vector4 backgroundVector = sourcePixels[x, y].ToVector4();
Vector4 sourceVector = applicator.GetColor(currentPoint).ToVector4();
Vector4 finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity);
finalColor.W = backgroundVector.W;
TColor packed = default(TColor);
packed.PackFromVector4(finalColor);
sourcePixels[x, y] = packed;
}
}
}
finally
{
arrayPool.Return(buffer);
}
});
if (this.options.Antialias)
{
// we only need to do the X can for antialiasing purposes
Parallel.For(
minX,
maxX,
this.ParallelOptions,
x =>
{
Vector2[] buffer = arrayPool.Rent(maxIntersections);
try
{
Vector2 left = new Vector2(x, polyStartY);
Vector2 right = new Vector2(x, polyEndY);
// foreach line we get all the points where this line crosses the polygon
int pointsFound = this.poly.FindIntersections(left, right, buffer, maxIntersections, 0);
if (pointsFound == 0)
{
arrayPool.Return(buffer);
// nothign on this line skip
return;
}
QuickSortY(buffer, pointsFound);
int currentIntersection = 0;
float nextPoint = buffer[0].Y;
float lastPoint = left.Y;
bool isInside = false;
// every odd point is the start of a line
Vector2 currentPoint = default(Vector2);
for (int y = minY; y < maxY; y++)
{
currentPoint.X = x;
currentPoint.Y = y;
if (!isInside)
{
if (y < (nextPoint - DrawPadding) && y > (lastPoint + DrawPadding))
{
if (nextPoint == right.Y)
{
// we are in the ends run skip it
y = maxY;
continue;
}
// lets just jump forward
y = (int)Math.Floor(nextPoint) - DrawPadding;
}
}
else
{
if (y < nextPoint - DrawPadding)
{
if (nextPoint == right.Y)
{
// we are in the ends run skip it
y = maxY;
continue;
}
// lets just jump forward
y = (int)Math.Floor(nextPoint);
}
}
bool onCorner = false;
if (y >= nextPoint)
{
currentIntersection++;
lastPoint = nextPoint;
if (currentIntersection == pointsFound)
{
nextPoint = right.Y;
}
else
{
nextPoint = buffer[currentIntersection].Y;
// double point from a corner flip the bit back and move on again
if (nextPoint == lastPoint)
{
onCorner = true;
isInside ^= true;
currentIntersection++;
if (currentIntersection == pointsFound)
{
nextPoint = right.Y;
}
else
{
nextPoint = buffer[currentIntersection].Y;
}
}
}
isInside ^= true;
}
float opacity = 1;
if (!isInside && !onCorner)
{
if (this.options.Antialias)
{
float distance = float.MaxValue;
if (y == lastPoint || y == nextPoint)
{
// we are to far away from the line
distance = 0;
}
else if (nextPoint - AntialiasFactor < y)
{
// we are near the left of the line
distance = nextPoint - y;
}
else if (lastPoint + AntialiasFactor > y)
{
// we are near the right of the line
distance = y - lastPoint;
}
else
{
// we are to far away from the line
continue;
}
opacity = 1 - (distance / AntialiasFactor);
}
else
{
continue;
}
}
// don't set full opactiy color as it will have been gotten by the first scan
if (opacity > Constants.Epsilon && opacity < 1)
{
Vector4 backgroundVector = sourcePixels[x, y].ToVector4();
Vector4 sourceVector = applicator.GetColor(currentPoint).ToVector4();
Vector4 finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity);
finalColor.W = backgroundVector.W;
TColor packed = default(TColor);
packed.PackFromVector4(finalColor);
sourcePixels[x, y] = packed;
}
}
}
finally
{
arrayPool.Return(buffer);
}
});
}
}
}
/// <summary>
/// Returns the correct alpha value for the given distance.
/// </summary>
/// <param name="distance">
/// The distance.
/// </param>
/// <returns>
/// The <see cref="float"/>.
/// </returns>
private float Opacity(float distance)
private static void Swap(Vector2[] data, int left, int right)
{
Vector2 tmp = data[left];
data[left] = data[right];
data[right] = tmp;
}
private static void QuickSortY(Vector2[] data, int size)
{
if (distance <= 0)
QuickSortY(data, 0, size - 1);
}
private static void QuickSortY(Vector2[] data, int lo, int hi)
{
if (lo < hi)
{
return 1;
int p = PartitionY(data, lo, hi);
QuickSortY(data, lo, p);
QuickSortY(data, p + 1, hi);
}
}
private static void QuickSortX(Vector2[] data, int size)
{
QuickSortX(data, 0, size - 1);
}
if (this.options.Antialias && distance < AntialiasFactor)
private static void QuickSortX(Vector2[] data, int lo, int hi)
{
if (lo < hi)
{
return 1 - (distance / AntialiasFactor);
int p = PartitionX(data, lo, hi);
QuickSortX(data, lo, p);
QuickSortX(data, p + 1, hi);
}
}
return 0;
private static int PartitionX(Vector2[] data, int lo, int hi)
{
float pivot = data[lo].X;
int i = lo - 1;
int j = hi + 1;
while (true)
{
do
{
i = i + 1;
}
while (data[i].X < pivot);
do
{
j = j - 1;
}
while (data[j].X > pivot);
if (i >= j)
{
return j;
}
Swap(data, i, j);
}
}
private static int PartitionY(Vector2[] data, int lo, int hi)
{
float pivot = data[lo].Y;
int i = lo - 1;
int j = hi + 1;
while (true)
{
do
{
i = i + 1;
}
while (data[i].Y < pivot);
do
{
j = j - 1;
}
while (data[j].Y > pivot);
if (i >= j)
{
return j;
}
Swap(data, i, j);
}
}
}
}

25
src/ImageSharp.Drawing/Shapes/BezierPolygon.cs

@ -34,6 +34,14 @@ namespace ImageSharp.Drawing.Shapes
/// </value>
public RectangleF Bounds => this.innerPolygon.Bounds;
/// <summary>
/// Gets the maximum number intersections that a shape can have when testing a line.
/// </summary>
/// <value>
/// The maximum intersections.
/// </value>
public int MaxIntersections => this.innerPolygon.MaxIntersections;
/// <summary>
/// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds
/// </summary>
@ -43,6 +51,23 @@ namespace ImageSharp.Drawing.Shapes
/// </returns>
public float Distance(Vector2 point) => this.innerPolygon.Distance(point);
/// <summary>
/// Based on a line described by <paramref name="start"/> and <paramref name="end"/>
/// populate a buffer for all points on the polygon that the line intersects.
/// </summary>
/// <param name="start">The start point of the line.</param>
/// <param name="end">The end point of the line.</param>
/// <param name="buffer">The buffer that will be populated with intersections.</param>
/// <param name="count">The count.</param>
/// <param name="offset">The offset.</param>
/// <returns>
/// The number of intersections populated into the buffer.
/// </returns>
public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset)
{
return this.innerPolygon.FindIntersections(start, end, buffer, count, offset);
}
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>

70
src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs

@ -5,6 +5,7 @@
namespace ImageSharp.Drawing.Shapes
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
@ -44,12 +45,12 @@ namespace ImageSharp.Drawing.Shapes
Guard.NotNull(outlines, nameof(outlines));
Guard.MustBeGreaterThanOrEqualTo(outlines.Length, 1, nameof(outlines));
this.FixAndSetShapes(outlines, holes);
this.MaxIntersections = this.FixAndSetShapes(outlines, holes);
var minX = this.shapes.Min(x => x.Bounds.Left);
var maxX = this.shapes.Max(x => x.Bounds.Right);
var minY = this.shapes.Min(x => x.Bounds.Top);
var maxY = this.shapes.Max(x => x.Bounds.Bottom);
float minX = this.shapes.Min(x => x.Bounds.Left);
float maxX = this.shapes.Max(x => x.Bounds.Right);
float minY = this.shapes.Min(x => x.Bounds.Top);
float maxY = this.shapes.Max(x => x.Bounds.Bottom);
this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY);
}
@ -62,6 +63,14 @@ namespace ImageSharp.Drawing.Shapes
/// </value>
public RectangleF Bounds { get; }
/// <summary>
/// Gets the maximum number intersections that a shape can have when testing a line.
/// </summary>
/// <value>
/// The maximum intersections.
/// </value>
public int MaxIntersections { get; }
/// <summary>
/// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds
/// </summary>
@ -80,7 +89,7 @@ namespace ImageSharp.Drawing.Shapes
bool inside = false;
foreach (IShape shape in this.shapes)
{
var d = shape.Distance(point);
float d = shape.Distance(point);
if (d <= 0)
{
@ -103,6 +112,33 @@ namespace ImageSharp.Drawing.Shapes
return dist;
}
/// <summary>
/// Based on a line described by <paramref name="start"/> and <paramref name="end"/>
/// populate a buffer for all points on all the polygons, that make up this complex shape,
/// that the line intersects.
/// </summary>
/// <param name="start">The start point of the line.</param>
/// <param name="end">The end point of the line.</param>
/// <param name="buffer">The buffer that will be populated with intersections.</param>
/// <param name="count">The count.</param>
/// <param name="offset">The offset.</param>
/// <returns>
/// The number of intersections populated into the buffer.
/// </returns>
public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset)
{
int totalAdded = 0;
for (int i = 0; i < this.shapes.Length; i++)
{
int added = this.shapes[i].FindIntersections(start, end, buffer, count, offset);
count -= added;
offset += added;
totalAdded += added;
}
return totalAdded;
}
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
@ -136,7 +172,7 @@ namespace ImageSharp.Drawing.Shapes
}
else
{
foreach (var path in shape)
foreach (IPath path in shape)
{
clipper.AddPath(
path,
@ -147,7 +183,7 @@ namespace ImageSharp.Drawing.Shapes
private void AddPoints(Clipper clipper, IEnumerable<IShape> shapes, PolyType polyType)
{
foreach (var shape in shapes)
foreach (IShape shape in shapes)
{
this.AddPoints(clipper, shape, polyType);
}
@ -167,28 +203,28 @@ namespace ImageSharp.Drawing.Shapes
else
{
// convert the Clipper Contour from scaled ints back down to the origional size (this is going to be lossy but not significantly)
var polygon = new Polygon(new Paths.LinearLineSegment(tree.Contour.ToArray()));
Polygon polygon = new Polygon(new Paths.LinearLineSegment(tree.Contour.ToArray()));
shapes.Add(polygon);
paths.Add(polygon);
}
}
foreach (var c in tree.Children)
foreach (PolyNode c in tree.Children)
{
this.ExtractOutlines(c, shapes, paths);
}
}
private void FixAndSetShapes(IEnumerable<IShape> outlines, IEnumerable<IShape> holes)
private int FixAndSetShapes(IEnumerable<IShape> outlines, IEnumerable<IShape> holes)
{
var clipper = new Clipper();
Clipper clipper = new Clipper();
// add the outlines and the holes to clipper, scaling up from the float source to the int based system clipper uses
this.AddPoints(clipper, outlines, PolyType.Subject);
this.AddPoints(clipper, holes, PolyType.Clip);
var tree = clipper.Execute();
PolyTree tree = clipper.Execute();
List<IShape> shapes = new List<IShape>();
List<IPath> paths = new List<IPath>();
@ -197,6 +233,14 @@ namespace ImageSharp.Drawing.Shapes
this.ExtractOutlines(tree, shapes, paths);
this.shapes = shapes.ToArray();
this.paths = paths.ToArray();
int intersections = 0;
foreach (IShape s in this.shapes)
{
intersections += s.MaxIntersections;
}
return intersections;
}
}
}

22
src/ImageSharp.Drawing/Shapes/IShape.cs

@ -22,6 +22,14 @@ namespace ImageSharp.Drawing.Shapes
/// </value>
RectangleF Bounds { get; }
/// <summary>
/// Gets the maximum number intersections that a shape can have when testing a line.
/// </summary>
/// <value>
/// The maximum intersections.
/// </value>
int MaxIntersections { get; }
/// <summary>
/// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds
/// </summary>
@ -30,5 +38,19 @@ namespace ImageSharp.Drawing.Shapes
/// Returns the distance from the shape to the point
/// </returns>
float Distance(Vector2 point);
/// <summary>
/// Based on a line described by <paramref name="start"/> and <paramref name="end"/>
/// populate a buffer for all points on the polygon that the line intersects.
/// </summary>
/// <param name="start">The start point of the line.</param>
/// <param name="end">The end point of the line.</param>
/// <param name="buffer">The buffer that will be populated with intersections.</param>
/// <param name="count">The count.</param>
/// <param name="offset">The offset.</param>
/// <returns>
/// The number of intersections populated into the buffer.
/// </returns>
int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset);
}
}

31
src/ImageSharp.Drawing/Shapes/LinearPolygon.cs

@ -34,6 +34,20 @@ namespace ImageSharp.Drawing.Shapes
/// </value>
public RectangleF Bounds => this.innerPolygon.Bounds;
/// <summary>
/// Gets the maximum number intersections that a shape can have when testing a line.
/// </summary>
/// <value>
/// The maximum intersections.
/// </value>
public int MaxIntersections
{
get
{
return this.innerPolygon.MaxIntersections;
}
}
/// <summary>
/// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds
/// </summary>
@ -43,6 +57,23 @@ namespace ImageSharp.Drawing.Shapes
/// </returns>
public float Distance(Vector2 point) => this.innerPolygon.Distance(point);
/// <summary>
/// Based on a line described by <paramref name="start"/> and <paramref name="end"/>
/// populate a buffer for all points on the polygon that the line intersects.
/// </summary>
/// <param name="start">The start point of the line.</param>
/// <param name="end">The end point of the line.</param>
/// <param name="buffer">The buffer that will be populated with intersections.</param>
/// <param name="count">The count.</param>
/// <param name="offset">The offset.</param>
/// <returns>
/// The number of intersections populated into the buffer.
/// </returns>
public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset)
{
return this.innerPolygon.FindIntersections(start, end, buffer, count, offset);
}
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>

27
src/ImageSharp.Drawing/Shapes/Polygon.cs

@ -63,6 +63,14 @@ namespace ImageSharp.Drawing.Shapes
/// </value>
public bool IsClosed => true;
/// <summary>
/// Gets the maximum number intersections that a shape can have when testing a line.
/// </summary>
/// <value>
/// The maximum intersections.
/// </value>
public int MaxIntersections => this.innerPath.Points.Length;
/// <summary>
/// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds
/// </summary>
@ -74,7 +82,7 @@ namespace ImageSharp.Drawing.Shapes
{
bool isInside = this.innerPath.PointInPolygon(point);
var distance = this.innerPath.DistanceFromPath(point).DistanceFromPath;
float distance = this.innerPath.DistanceFromPath(point).DistanceFromPath;
if (isInside)
{
return -distance;
@ -127,5 +135,22 @@ namespace ImageSharp.Drawing.Shapes
{
return this.innerPath.Points;
}
/// <summary>
/// Based on a line described by <paramref name="start" /> and <paramref name="end" />
/// populate a buffer for all points on the polygon that the line intersects.
/// </summary>
/// <param name="start">The start point of the line.</param>
/// <param name="end">The end point of the line.</param>
/// <param name="buffer">The buffer that will be populated with intersections.</param>
/// <param name="count">The count.</param>
/// <param name="offset">The offset.</param>
/// <returns>
/// The number of intersections populated into the buffer.
/// </returns>
public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset)
{
return this.innerPath.FindIntersections(start, end, buffer, count, offset);
}
}
}

46
src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs

@ -79,6 +79,14 @@ namespace ImageSharp.Drawing.Shapes
/// </value>
public float Length { get; }
/// <summary>
/// Gets the maximum number intersections that a shape can have when testing a line.
/// </summary>
/// <value>
/// The maximum intersections.
/// </value>
public int MaxIntersections => 4;
/// <summary>
/// Calculates the distance along and away from the path for a specified point.
/// </summary>
@ -102,7 +110,7 @@ namespace ImageSharp.Drawing.Shapes
public float Distance(Vector2 point)
{
bool insidePoly;
var result = this.Distance(point, true, out insidePoly);
PointInfo result = this.Distance(point, true, out insidePoly);
// invert the distance from path when inside
return insidePoly ? -result.DistanceFromPath : result.DistanceFromPath;
@ -141,6 +149,42 @@ namespace ImageSharp.Drawing.Shapes
return this.points;
}
/// <summary>
/// Based on a line described by <paramref name="start"/> and <paramref name="end"/>
/// populate a buffer for all points on the edges of the <see cref="RectangularPolygon"/>
/// that the line intersects.
/// </summary>
/// <param name="start">The start point of the line.</param>
/// <param name="end">The end point of the line.</param>
/// <param name="buffer">The buffer that will be populated with intersections.</param>
/// <param name="count">The count.</param>
/// <param name="offset">The offset.</param>
/// <returns>
/// The number of intersections populated into the buffer.
/// </returns>
public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset)
{
int discovered = 0;
Vector2 startPoint = Vector2.Clamp(start, this.topLeft, this.bottomRight);
Vector2 endPoint = Vector2.Clamp(end, this.topLeft, this.bottomRight);
if (startPoint == Vector2.Clamp(startPoint, start, end))
{
// if start closest is within line then its a valid point
discovered++;
buffer[offset++] = startPoint;
}
if (endPoint == Vector2.Clamp(endPoint, start, end))
{
// if start closest is within line then its a valid point
discovered++;
buffer[offset++] = endPoint;
}
return discovered;
}
private PointInfo Distance(Vector2 point, bool getDistanceAwayOnly, out bool isInside)
{
// point in rectangle

6
tests/ImageSharp.Tests/Drawing/SolidComplexPolygonTests.cs

@ -48,6 +48,8 @@ namespace ImageSharp.Tests.Drawing
Assert.Equal(Color.HotPink, sourcePixels[50, 50]);
Assert.Equal(Color.HotPink, sourcePixels[35, 100]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
//inside hole
@ -88,6 +90,8 @@ namespace ImageSharp.Tests.Drawing
Assert.Equal(Color.HotPink, sourcePixels[50, 50]);
Assert.Equal(Color.HotPink, sourcePixels[35, 100]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
//inside hole
@ -131,6 +135,8 @@ namespace ImageSharp.Tests.Drawing
Assert.Equal(mergedColor, sourcePixels[50, 50]);
Assert.Equal(mergedColor, sourcePixels[35, 100]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
//inside hole

2
tests/ImageSharp.Tests/Drawing/SolidPolygonTests.cs

@ -31,7 +31,7 @@ namespace ImageSharp.Tests.Drawing
{
image
.BackgroundColor(Color.Blue)
.FillPolygon(Color.HotPink, simplePath)
.FillPolygon(Color.HotPink, simplePath, new GraphicsOptions(true))
.Save(output);
}

Loading…
Cancel
Save