// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // namespace ImageSharp.Drawing.Pens { using System.Numerics; using ImageSharp.Drawing.Brushes; using ImageSharp.PixelFormats; using Processors; /// /// Provides a pen that can apply a pattern to a line with a set brush and thickness /// /// The type of the color. /// /// The pattern will be in to the form of new float[]{ 1f, 2f, 0.5f} this will be /// converted into a pattern that is 3.5 times longer that the width with 3 sections /// section 1 will be width long (making a square) and will be filled by the brush /// section 2 will be width * 2 long and will be empty /// section 3 will be width/2 long and will be filled /// the the pattern will imidiatly repeat without gap. /// public class Pen : IPen where TPixel : struct, IPixel { private static readonly float[] EmptyPattern = new float[0]; private readonly float[] pattern; /// /// Initializes a new instance of the class. /// /// The color. /// The width. /// The pattern. public Pen(TPixel color, float width, float[] pattern) : this(new SolidBrush(color), width, pattern) { } /// /// Initializes a new instance of the class. /// /// The brush. /// The width. /// The pattern. public Pen(IBrush brush, float width, float[] pattern) { this.Brush = brush; this.Width = width; this.pattern = pattern; } /// /// Initializes a new instance of the class. /// /// The color. /// The width. public Pen(TPixel color, float width) : this(new SolidBrush(color), width) { } /// /// Initializes a new instance of the class. /// /// The brush. /// The width. public Pen(IBrush brush, float width) : this(brush, width, EmptyPattern) { } /// /// Initializes a new instance of the class. /// /// The pen. internal Pen(Pen pen) : this(pen.Brush, pen.Width, pen.pattern) { } /// /// Gets the brush. /// /// /// The brush. /// public IBrush Brush { get; } /// /// Gets the width. /// /// /// The width. /// public float Width { get; } /// /// Creates the applicator for applying this pen to an Image /// /// The source image. /// The region the pen will be applied to. /// The Graphics options /// /// Returns a the applicator for the pen. /// /// /// 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 PenApplicator CreateApplicator(ImageBase source, RectangleF region, GraphicsOptions options) { if (this.pattern == null || this.pattern.Length < 2) { // if there is only one item in the pattern then 100% of it will // be solid so use the quicker applicator return new SolidPenApplicator(source, this.Brush, region, this.Width, options); } return new PatternPenApplicator(source, this.Brush, region, this.Width, this.pattern, options); } private class SolidPenApplicator : PenApplicator { private readonly BrushApplicator brush; private readonly float halfWidth; public SolidPenApplicator(ImageBase sourcePixels, IBrush brush, RectangleF region, float width, GraphicsOptions options) { this.brush = brush.CreateApplicator(sourcePixels, region, options); this.halfWidth = width / 2; this.RequiredRegion = RectangleF.Inflate(region, width, width); } public override RectangleF RequiredRegion { get; } public override void Dispose() { this.brush.Dispose(); } public override ColoredPointInfo GetColor(int x, int y, PointInfo info) { var result = default(ColoredPointInfo); result.Color = this.brush[x, y]; if (info.DistanceFromPath < this.halfWidth) { // inside strip result.DistanceFromElement = 0; } else { result.DistanceFromElement = info.DistanceFromPath - this.halfWidth; } return result; } } private class PatternPenApplicator : PenApplicator { private readonly BrushApplicator brush; private readonly float halfWidth; private readonly float[] pattern; private readonly float totalLength; public PatternPenApplicator(ImageBase source, IBrush brush, RectangleF region, float width, float[] pattern, GraphicsOptions options) { this.brush = brush.CreateApplicator(source, region, options); this.halfWidth = width / 2; this.totalLength = 0; this.pattern = new float[pattern.Length + 1]; this.pattern[0] = 0; for (int i = 0; i < pattern.Length; i++) { this.totalLength += pattern[i] * width; this.pattern[i + 1] = this.totalLength; } this.RequiredRegion = RectangleF.Inflate(region, width, width); } public override RectangleF RequiredRegion { get; } public override void Dispose() { this.brush.Dispose(); } public override ColoredPointInfo GetColor(int x, int y, PointInfo info) { var infoResult = default(ColoredPointInfo); infoResult.DistanceFromElement = float.MaxValue; // is really outside the element float length = info.DistanceAlongPath % this.totalLength; // we can treat the DistanceAlongPath and DistanceFromPath as x,y coords for the pattern // we need to calcualte the distance from the outside edge of the pattern // and set them on the ColoredPointInfo along with the color. infoResult.Color = this.brush[x, y]; float distanceWAway = 0; if (info.DistanceFromPath < this.halfWidth) { // inside strip distanceWAway = 0; } else { distanceWAway = info.DistanceFromPath - this.halfWidth; } for (int i = 0; i < this.pattern.Length - 1; i++) { float start = this.pattern[i]; float end = this.pattern[i + 1]; if (length >= start && length < end) { // in section if (i % 2 == 0) { // solid part return the maxDistance infoResult.DistanceFromElement = distanceWAway; return infoResult; } else { // this is a none solid part float distanceFromStart = length - start; float distanceFromEnd = end - length; float closestEdge = MathF.Min(distanceFromStart, distanceFromEnd); float distanceAcross = closestEdge; if (distanceWAway > 0) { infoResult.DistanceFromElement = new Vector2(distanceAcross, distanceWAway).Length(); } else { infoResult.DistanceFromElement = closestEdge; } return infoResult; } } } return infoResult; } } } }