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