diff --git a/src/ImageSharp.Drawing/Brushes/IBrush.cs b/src/ImageSharp.Drawing/Brushes/IBrush.cs index 1eea302a56..b281802048 100644 --- a/src/ImageSharp.Drawing/Brushes/IBrush.cs +++ b/src/ImageSharp.Drawing/Brushes/IBrush.cs @@ -14,7 +14,7 @@ namespace ImageSharp.Drawing /// /// The pixel format. /// - /// A brush is a simple class that will return an that will perform the + /// A brush is a simple class that will return an that will perform the /// logic for converting a pixel location to a . /// public interface IBrush @@ -32,6 +32,6 @@ namespace ImageSharp.Drawing /// The when being applied to things like shapes would usually be the /// bounding box of the shape not necessarily the bounds of the whole image /// - IBrushApplicator CreateApplicator(PixelAccessor pixelSource, RectangleF region); + BrushApplicator CreateApplicator(PixelAccessor pixelSource, RectangleF region); } } \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Brushes/ImageBrush{TColor}.cs b/src/ImageSharp.Drawing/Brushes/ImageBrush{TColor}.cs index 5daf03b935..9ce235a847 100644 --- a/src/ImageSharp.Drawing/Brushes/ImageBrush{TColor}.cs +++ b/src/ImageSharp.Drawing/Brushes/ImageBrush{TColor}.cs @@ -32,7 +32,7 @@ namespace ImageSharp.Drawing.Brushes } /// - public IBrushApplicator CreateApplicator(PixelAccessor sourcePixels, RectangleF region) + public BrushApplicator CreateApplicator(PixelAccessor sourcePixels, RectangleF region) { return new ImageBrushApplicator(this.image, region); } @@ -40,7 +40,7 @@ namespace ImageSharp.Drawing.Brushes /// /// The image brush applicator. /// - private class ImageBrushApplicator : IBrushApplicator + private class ImageBrushApplicator : BrushApplicator { /// /// The source pixel accessor. @@ -86,7 +86,7 @@ namespace ImageSharp.Drawing.Brushes /// /// The color /// - 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 } /// - public void Dispose() + public override void Dispose() { this.source.Dispose(); } diff --git a/src/ImageSharp.Drawing/Brushes/PatternBrush{TColor}.cs b/src/ImageSharp.Drawing/Brushes/PatternBrush{TColor}.cs index 76b11236aa..7749f5ba84 100644 --- a/src/ImageSharp.Drawing/Brushes/PatternBrush{TColor}.cs +++ b/src/ImageSharp.Drawing/Brushes/PatternBrush{TColor}.cs @@ -95,7 +95,7 @@ namespace ImageSharp.Drawing.Brushes } /// - public IBrushApplicator CreateApplicator(PixelAccessor sourcePixels, RectangleF region) + public BrushApplicator CreateApplicator(PixelAccessor sourcePixels, RectangleF region) { return new PatternBrushApplicator(this.pattern, this.stride); } @@ -103,7 +103,7 @@ namespace ImageSharp.Drawing.Brushes /// /// The pattern brush applicator. /// - private class PatternBrushApplicator : IBrushApplicator + private class PatternBrushApplicator : BrushApplicator { /// /// The patter x-length. @@ -139,7 +139,7 @@ namespace ImageSharp.Drawing.Brushes /// /// The color /// - 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 } /// - public void Dispose() + public override void Dispose() { // noop } diff --git a/src/ImageSharp.Drawing/Brushes/Processors/IBrushApplicator.cs b/src/ImageSharp.Drawing/Brushes/Processors/BrushApplicator.cs similarity index 61% rename from src/ImageSharp.Drawing/Brushes/Processors/IBrushApplicator.cs rename to src/ImageSharp.Drawing/Brushes/Processors/BrushApplicator.cs index 9b09f87db3..885be57157 100644 --- a/src/ImageSharp.Drawing/Brushes/Processors/IBrushApplicator.cs +++ b/src/ImageSharp.Drawing/Brushes/Processors/BrushApplicator.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // @@ -13,14 +13,19 @@ namespace ImageSharp.Drawing.Processors /// /// The pixel format. /// - public interface IBrushApplicator : IDisposable // disposable will be required if/when there is an ImageBrush + public abstract class BrushApplicator : IDisposable // disposable will be required if/when there is an ImageBrush where TColor : struct, IPackedPixel, IEquatable { + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public abstract void Dispose(); + /// /// Gets the color for a single pixel. /// /// The point. /// The color - TColor GetColor(Vector2 point); + public abstract TColor GetColor(Vector2 point); } } diff --git a/src/ImageSharp.Drawing/Brushes/RecolorBrush{TColor}.cs b/src/ImageSharp.Drawing/Brushes/RecolorBrush{TColor}.cs index 35f72c5bfa..7149f22a01 100644 --- a/src/ImageSharp.Drawing/Brushes/RecolorBrush{TColor}.cs +++ b/src/ImageSharp.Drawing/Brushes/RecolorBrush{TColor}.cs @@ -55,7 +55,7 @@ namespace ImageSharp.Drawing.Brushes public TColor TargetColor { get; } /// - public IBrushApplicator CreateApplicator(PixelAccessor sourcePixels, RectangleF region) + public BrushApplicator CreateApplicator(PixelAccessor sourcePixels, RectangleF region) { return new RecolorBrushApplicator(sourcePixels, this.SourceColor, this.TargetColor, this.Threshold); } @@ -63,7 +63,7 @@ namespace ImageSharp.Drawing.Brushes /// /// The recolor brush applicator. /// - private class RecolorBrushApplicator : IBrushApplicator + private class RecolorBrushApplicator : BrushApplicator { /// /// The source pixel accessor. @@ -113,7 +113,7 @@ namespace ImageSharp.Drawing.Brushes /// /// The color /// - 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 } /// - public void Dispose() + public override void Dispose() { - // we didn't make the lock on the PixelAccessor we shouldn't release it. } } } diff --git a/src/ImageSharp.Drawing/Brushes/SolidBrush{TColor}.cs b/src/ImageSharp.Drawing/Brushes/SolidBrush{TColor}.cs index ac3986bba9..c3e3113992 100644 --- a/src/ImageSharp.Drawing/Brushes/SolidBrush{TColor}.cs +++ b/src/ImageSharp.Drawing/Brushes/SolidBrush{TColor}.cs @@ -40,7 +40,7 @@ namespace ImageSharp.Drawing.Brushes public TColor Color => this.color; /// - public IBrushApplicator CreateApplicator(PixelAccessor sourcePixels, RectangleF region) + public BrushApplicator CreateApplicator(PixelAccessor sourcePixels, RectangleF region) { return new SolidBrushApplicator(this.color); } @@ -48,7 +48,7 @@ namespace ImageSharp.Drawing.Brushes /// /// The solid brush applicator. /// - private class SolidBrushApplicator : IBrushApplicator + private class SolidBrushApplicator : BrushApplicator { /// /// The solid color. @@ -71,13 +71,13 @@ namespace ImageSharp.Drawing.Brushes /// /// The color /// - public TColor GetColor(Vector2 point) + public override TColor GetColor(Vector2 point) { return this.color; } /// - public void Dispose() + public override void Dispose() { // noop } diff --git a/src/ImageSharp.Drawing/Paths/InternalPath.cs b/src/ImageSharp.Drawing/Paths/InternalPath.cs index 52d43b6e8c..36a0704cbe 100644 --- a/src/ImageSharp.Drawing/Paths/InternalPath.cs +++ b/src/ImageSharp.Drawing/Paths/InternalPath.cs @@ -14,6 +14,11 @@ namespace ImageSharp.Drawing.Paths /// internal class InternalPath { + /// + /// The maximum vector + /// + private static readonly Vector2 MaxVector = new Vector2(float.MaxValue); + /// /// The locker. /// @@ -164,7 +169,47 @@ namespace ImageSharp.Drawing.Paths } /// - /// Points the in polygon. + /// Based on a line described by and + /// populate a buffer for all points on the path that the line intersects. + /// + /// The start. + /// The end. + /// The buffer. + /// The count. + /// The offset. + /// number iof intersections hit + 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; + } + + /// + /// Determines if the specified point is inside or outside the path. /// /// The point. /// Returns true if the point is inside the closed path. @@ -203,6 +248,111 @@ namespace ImageSharp.Drawing.Paths return oddNodes; } + /// + /// Determins if the bounding box for 2 lines + /// described by and + /// and and overlap. + /// + /// The line1 start. + /// The line1 end. + /// The line2 start. + /// The line2 end. + /// Returns true it the bounding box of the 2 lines intersect + 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; + } + + /// + /// Finds the point on line described by and + /// that intersects with line described by and + /// + /// The line1 start. + /// The line1 end. + /// The line2 start. + /// The line2 end. + /// + /// A describing the point that the 2 lines cross or if they do not. + /// + 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; + } + /// /// Simplifies the collection of segments. /// diff --git a/src/ImageSharp.Drawing/Pens/IPen.cs b/src/ImageSharp.Drawing/Pens/IPen.cs index 46b4a2c9d6..0cf473427f 100644 --- a/src/ImageSharp.Drawing/Pens/IPen.cs +++ b/src/ImageSharp.Drawing/Pens/IPen.cs @@ -26,6 +26,6 @@ namespace ImageSharp.Drawing.Pens /// /// The when being applied to things like shapes would usually be the bounding box of the shape not necessarily the shape of the whole image. /// - IPenApplicator CreateApplicator(PixelAccessor pixelSource, RectangleF region); + PenApplicator CreateApplicator(PixelAccessor pixelSource, RectangleF region); } } diff --git a/src/ImageSharp.Drawing/Pens/Pen{TColor}.cs b/src/ImageSharp.Drawing/Pens/Pen{TColor}.cs index 0af039ac77..a08c7a7faf 100644 --- a/src/ImageSharp.Drawing/Pens/Pen{TColor}.cs +++ b/src/ImageSharp.Drawing/Pens/Pen{TColor}.cs @@ -111,7 +111,7 @@ namespace ImageSharp.Drawing.Pens /// The when being applied to things like shapes would ussually be the /// bounding box of the shape not necorserrally the shape of the whole image /// - public IPenApplicator CreateApplicator(PixelAccessor sourcePixels, RectangleF region) + public PenApplicator CreateApplicator(PixelAccessor 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 + private class SolidPenApplicator : PenApplicator { - private readonly IBrushApplicator brush; + private readonly BrushApplicator brush; private readonly float halfWidth; public SolidPenApplicator(PixelAccessor sourcePixels, IBrush 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 GetColor(PointInfo info) + public override ColoredPointInfo GetColor(PointInfo info) { var result = default(ColoredPointInfo); result.Color = this.brush.GetColor(info.SearchPoint); @@ -164,9 +164,9 @@ namespace ImageSharp.Drawing.Pens } } - private class PatternPenApplicator : IPenApplicator + private class PatternPenApplicator : PenApplicator { - private readonly IBrushApplicator brush; + private readonly BrushApplicator 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 GetColor(PointInfo info) + public override ColoredPointInfo GetColor(PointInfo info) { var infoResult = default(ColoredPointInfo); infoResult.DistanceFromElement = float.MaxValue; // is really outside the element diff --git a/src/ImageSharp.Drawing/Pens/Processors/IPenApplicator.cs b/src/ImageSharp.Drawing/Pens/Processors/PenApplicator.cs similarity index 68% rename from src/ImageSharp.Drawing/Pens/Processors/IPenApplicator.cs rename to src/ImageSharp.Drawing/Pens/Processors/PenApplicator.cs index 7159dfeec3..e07b969495 100644 --- a/src/ImageSharp.Drawing/Pens/Processors/IPenApplicator.cs +++ b/src/ImageSharp.Drawing/Pens/Processors/PenApplicator.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // @@ -12,7 +12,7 @@ namespace ImageSharp.Drawing.Processors /// primitive that converts a into a color and a distance away from the drawable part of the path. /// /// The type of the color. - public interface IPenApplicator : IDisposable + public abstract class PenApplicator : IDisposable where TColor : struct, IPackedPixel, IEquatable { /// @@ -21,13 +21,18 @@ namespace ImageSharp.Drawing.Processors /// /// The required region. /// - RectangleF RequiredRegion { get; } + public abstract RectangleF RequiredRegion { get; } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public abstract void Dispose(); /// /// Gets a from a point represented by a . /// /// The information to extract color details about. /// Returns the color details and distance from a solid bit of the line. - ColoredPointInfo GetColor(PointInfo info); + public abstract ColoredPointInfo GetColor(PointInfo info); } } diff --git a/src/ImageSharp.Drawing/Processors/DrawPathProcessor.cs b/src/ImageSharp.Drawing/Processors/DrawPathProcessor.cs index 7fcdb95a6f..f7bdcb6895 100644 --- a/src/ImageSharp.Drawing/Processors/DrawPathProcessor.cs +++ b/src/ImageSharp.Drawing/Processors/DrawPathProcessor.cs @@ -84,7 +84,7 @@ namespace ImageSharp.Drawing.Processors protected override void OnApply(ImageBase source, Rectangle sourceRectangle) { using (PixelAccessor sourcePixels = source.Lock()) - using (IPenApplicator applicator = this.pen.CreateApplicator(sourcePixels, this.region)) + using (PenApplicator applicator = this.pen.CreateApplicator(sourcePixels, this.region)) { var rect = RectangleF.Ceiling(applicator.RequiredRegion); diff --git a/src/ImageSharp.Drawing/Processors/FillProcessor.cs b/src/ImageSharp.Drawing/Processors/FillProcessor.cs index 309d3670f5..dc87e6da6c 100644 --- a/src/ImageSharp.Drawing/Processors/FillProcessor.cs +++ b/src/ImageSharp.Drawing/Processors/FillProcessor.cs @@ -62,7 +62,7 @@ namespace ImageSharp.Drawing.Processors // for example If brush is SolidBrush then we could just get the color upfront // and skip using the IBrushApplicator?. using (PixelAccessor sourcePixels = source.Lock()) - using (IBrushApplicator applicator = this.brush.CreateApplicator(sourcePixels, sourceRectangle)) + using (BrushApplicator 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); diff --git a/src/ImageSharp.Drawing/Processors/FillShapeProcessor.cs b/src/ImageSharp.Drawing/Processors/FillShapeProcessor.cs index d0655341b7..36f89f8ec1 100644 --- a/src/ImageSharp.Drawing/Processors/FillShapeProcessor.cs +++ b/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 /// /// The brush. /// The shape. - /// The graphics options. + /// The options. public FillShapeProcessor(IBrush 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 arrayPool = ArrayPool.Shared; - if (minY > 0) - { - polyStartY = 0; - } + int maxIntersections = this.poly.MaxIntersections; using (PixelAccessor sourcePixels = source.Lock()) - using (IBrushApplicator applicator = this.fillColor.CreateApplicator(sourcePixels, rect)) + using (BrushApplicator 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); + } + }); + } } } - /// - /// Returns the correct alpha value for the given distance. - /// - /// - /// The distance. - /// - /// - /// The . - /// - 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); + } } } } \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Shapes/BezierPolygon.cs b/src/ImageSharp.Drawing/Shapes/BezierPolygon.cs index 0365588238..b69ded2075 100644 --- a/src/ImageSharp.Drawing/Shapes/BezierPolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/BezierPolygon.cs @@ -34,6 +34,14 @@ namespace ImageSharp.Drawing.Shapes /// public RectangleF Bounds => this.innerPolygon.Bounds; + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections => this.innerPolygon.MaxIntersections; + /// /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds /// @@ -43,6 +51,23 @@ namespace ImageSharp.Drawing.Shapes /// public float Distance(Vector2 point) => this.innerPolygon.Distance(point); + /// + /// Based on a line described by and + /// populate a buffer for all points on the polygon that the line intersects. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset) + { + return this.innerPolygon.FindIntersections(start, end, buffer, count, offset); + } + /// /// Returns an enumerator that iterates through the collection. /// diff --git a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs index d9b83d3415..fd709719c1 100644 --- a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs +++ b/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 /// public RectangleF Bounds { get; } + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections { get; } + /// /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds /// @@ -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; } + /// + /// Based on a line described by and + /// populate a buffer for all points on all the polygons, that make up this complex shape, + /// that the line intersects. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + 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; + } + /// /// Returns an enumerator that iterates through the collection. /// @@ -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 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 outlines, IEnumerable holes) + private int FixAndSetShapes(IEnumerable outlines, IEnumerable 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 shapes = new List(); List paths = new List(); @@ -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; } } } \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Shapes/IShape.cs b/src/ImageSharp.Drawing/Shapes/IShape.cs index 2640b33aa4..242e3bd8ec 100644 --- a/src/ImageSharp.Drawing/Shapes/IShape.cs +++ b/src/ImageSharp.Drawing/Shapes/IShape.cs @@ -22,6 +22,14 @@ namespace ImageSharp.Drawing.Shapes /// RectangleF Bounds { get; } + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + int MaxIntersections { get; } + /// /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds /// @@ -30,5 +38,19 @@ namespace ImageSharp.Drawing.Shapes /// Returns the distance from the shape to the point /// float Distance(Vector2 point); + + /// + /// Based on a line described by and + /// populate a buffer for all points on the polygon that the line intersects. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset); } } diff --git a/src/ImageSharp.Drawing/Shapes/LinearPolygon.cs b/src/ImageSharp.Drawing/Shapes/LinearPolygon.cs index 03069b6785..30a30c20f9 100644 --- a/src/ImageSharp.Drawing/Shapes/LinearPolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/LinearPolygon.cs @@ -34,6 +34,20 @@ namespace ImageSharp.Drawing.Shapes /// public RectangleF Bounds => this.innerPolygon.Bounds; + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections + { + get + { + return this.innerPolygon.MaxIntersections; + } + } + /// /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds /// @@ -43,6 +57,23 @@ namespace ImageSharp.Drawing.Shapes /// public float Distance(Vector2 point) => this.innerPolygon.Distance(point); + /// + /// Based on a line described by and + /// populate a buffer for all points on the polygon that the line intersects. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset) + { + return this.innerPolygon.FindIntersections(start, end, buffer, count, offset); + } + /// /// Returns an enumerator that iterates through the collection. /// diff --git a/src/ImageSharp.Drawing/Shapes/Polygon.cs b/src/ImageSharp.Drawing/Shapes/Polygon.cs index 6da27cf488..86c3c9ee43 100644 --- a/src/ImageSharp.Drawing/Shapes/Polygon.cs +++ b/src/ImageSharp.Drawing/Shapes/Polygon.cs @@ -63,6 +63,14 @@ namespace ImageSharp.Drawing.Shapes /// public bool IsClosed => true; + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections => this.innerPath.Points.Length; + /// /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds /// @@ -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; } + + /// + /// Based on a line described by and + /// populate a buffer for all points on the polygon that the line intersects. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset) + { + return this.innerPath.FindIntersections(start, end, buffer, count, offset); + } } } diff --git a/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs b/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs index f05dadc7cc..5002bee406 100644 --- a/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs @@ -79,6 +79,14 @@ namespace ImageSharp.Drawing.Shapes /// public float Length { get; } + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections => 4; + /// /// Calculates the distance along and away from the path for a specified point. /// @@ -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; } + /// + /// Based on a line described by and + /// populate a buffer for all points on the edges of the + /// that the line intersects. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + 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 diff --git a/tests/ImageSharp.Tests/Drawing/SolidComplexPolygonTests.cs b/tests/ImageSharp.Tests/Drawing/SolidComplexPolygonTests.cs index dd4cbfd2b9..cbd44d7d1c 100644 --- a/tests/ImageSharp.Tests/Drawing/SolidComplexPolygonTests.cs +++ b/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 diff --git a/tests/ImageSharp.Tests/Drawing/SolidPolygonTests.cs b/tests/ImageSharp.Tests/Drawing/SolidPolygonTests.cs index a70782478b..0d3b721d52 100644 --- a/tests/ImageSharp.Tests/Drawing/SolidPolygonTests.cs +++ b/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); }