Table of Contents

Class Path

Namespace
Egami.Drawing
Assembly
Egami.Drawing.dll

An outline of contours made of lines, quadratic and cubic Bézier curves, with the FillRule that says which points it covers when filled. A path never changes after it is built, so one path can be drawn from many threads and kept by any canvas backend. Coordinates are float pixels, y down. Build one with PathBuilder or a shape helper such as Circle(Vector2, float).

public sealed class Path
Inheritance
Path
Inherited Members

Examples

// A path is built once and never changes, so it can be drawn many times and from many threads.
Path heart = new PathBuilder()
    .MoveTo(150, 60)
    .CubicTo(150, 30, 100, 20, 90, 60)
    .CubicTo(80, 100, 130, 130, 150, 160)
    .CubicTo(170, 130, 220, 100, 210, 60)
    .CubicTo(200, 20, 150, 30, 150, 60)
    .Close()
    .ToPath();
canvas.FillPath(heart, Rgba8.Red);

// SVG's arc command, and the even-odd rule: an inner contour becomes a hole.
Path frame = new PathBuilder()
    .AddRect(new RectF(10, 10, 60, 60))
    .AddCircle(new Vector2(40, 40), 20)
    .MoveTo(10, 120)
    .ArcTo(radii: new Vector2(30, 20), rotationDegrees: 0, largeArc: false, clockwise: true, end: new Vector2(70, 120))
    .Close()
    .ToPath(FillRule.EvenOdd);
canvas.FillPath(frame, Rgba8.Black);

bool hit = heart.Contains(new Vector2(150, 100));   // hit testing on the curves themselves
RectF box = heart.TightBounds;                       // curves measured at their extremes

Properties

Bounds

Gets the bounds of every point, control points included: cheap, and never smaller than the outline.

public RectF Bounds { get; }

Property Value

RectF

Contours

Gets the contours, each as a path of its own with this path's fill rule.

public IReadOnlyList<Path> Contours { get; }

Property Value

IReadOnlyList<Path>

Empty

Gets a path with no contours.

public static Path Empty { get; }

Property Value

Path

FillRule

Gets which points the path covers when filled.

public FillRule FillRule { get; }

Property Value

FillRule

IsConvex

Gets a value indicating whether the path is one convex contour: every turn goes the same way and it winds around once. A path that is a line, a point or empty counts as convex.

public bool IsConvex { get; }

Property Value

bool

IsEmpty

Gets a value indicating whether the path draws nothing: it has no line or curve.

public bool IsEmpty { get; }

Property Value

bool

Points

Gets the points, in order: one for each move and line, two for each quadratic and three for each cubic.

public ReadOnlySpan<Vector2> Points { get; }

Property Value

ReadOnlySpan<Vector2>

TightBounds

Gets the exact bounds of the outline: curves are measured at their extremes, not their control points.

public RectF TightBounds { get; }

Property Value

RectF

Verbs

Gets the commands, in order.

public ReadOnlySpan<PathVerb> Verbs { get; }

Property Value

ReadOnlySpan<PathVerb>

Methods

Apply(PathEffect)

Returns the path an effect makes of this one, such as its dashes or its corners rounded.

public Path Apply(PathEffect effect)

Parameters

effect PathEffect

The effect.

Returns

Path

The new path.

Examples

// A hand-drawn chart line: jittered the same way on every run, then its corners rounded.
Path data = Path.Polygon([new(10, 80), new(50, 30), new(90, 60), new(130, 20), new(170, 50)], close: false);
PathEffect sketchy = PathEffect.Jitter(segmentLength: 6, deviation: 1.5f, seed: 7).Then(PathEffect.Corner(3));
using Raster<Rgba8> chart = Raster.Create(180, 100, Rgba8.White);
using (RasterCanvas<Rgba8> canvas = chart.CreateCanvas())
{
    canvas.StrokePath(data, new Stroke(2) { Effect = sketchy }, new Rgba8(40, 40, 40, 255));

    // Dots along the line, and hatching inside a circle.
    canvas.FillPath(data.Apply(PathEffect.Stamp(Path.Circle(Vector2.Zero, 2), advance: 12, style: StampStyle.Translate)), Rgba8.Black);
    canvas.FillPath(Path.Circle(new Vector2(150, 75), 20).Apply(PathEffect.Hatch(Matrix3x2.CreateScale(1, 5) * Matrix3x2.CreateRotation(0.6f), width: 1)), Rgba8.Black);
}

// A line that draws itself: trim it to a growing fraction of its length, frame by frame.
Path frame30 = data.Apply(PathEffect.Trim(0, 0.3f));

Arc(RectF, float, float)

Creates the open arc of an ellipse; 0 degrees is the right end of the horizontal axis and positive angles turn clockwise.

public static Path Arc(RectF oval, float startDegrees, float sweepDegrees)

Parameters

oval RectF

The rectangle the ellipse fills.

startDegrees float

Where the arc starts.

sweepDegrees float

How far it turns, at most a full turn either way.

Returns

Path

The path.

Circle(Vector2, float)

Creates a circle, clockwise.

public static Path Circle(Vector2 center, float radius)

Parameters

center Vector2

The center.

radius float

The radius.

Returns

Path

The path.

Combine(IEnumerable<Path>, PathOp, float)

Combines any number of paths with one operation in a single pass, as if it were applied in order, left to right: ((a op b) op c) and so on. Far faster than chaining Op(Path, PathOp, float) for many paths, since every outline is cut and sorted once. The result is as Op(Path, PathOp, float) describes.

public static Path Combine(IEnumerable<Path> paths, PathOp op, float tolerance = 0.05)

Parameters

paths IEnumerable<Path>

The paths; each keeps its own fill rule.

op PathOp

The operation.

tolerance float

How close, in path units, points must be to count as the same, as Op(Path, PathOp, float) describes.

Returns

Path

The combined path; empty when there are no paths.

Examples

// A frame: one round rectangle cut out of another, kept as curves, so a PDF or SVG backend writes it exactly.
Path frame = Path.RoundRect(new RoundRect(new RectF(0, 0, 200, 140), 24))
    .Difference(Path.RoundRect(new RoundRect(new RectF(20, 20, 160, 100), 12)));

// Many shapes in one pass, applied left to right; overlaps and self-intersections removed; an area grown by 4 units.
Path dots = Path.Combine([Path.Circle(new Vector2(40, 40), 30), Path.Circle(new Vector2(80, 40), 30), Path.Circle(new Vector2(60, 75), 30)], PathOp.Union);
Path clean = Path.Star(new Vector2(50, 50), 45, 20, 5).Simplify();
Path halo = dots.Inflate(4, LineJoin.Round);

Exceptions

ArgumentOutOfRangeException

op is not defined, or tolerance is not positive and finite.

Contains(Vector2)

Returns whether the filled path covers a point, by the winding number of the outline itself, curves included, and the fill rule. Open contours count as closed. A point exactly on the outline counts as inside, as in Skia.

public bool Contains(Vector2 point)

Parameters

point Vector2

The point.

Returns

bool

true if the point is covered.

Difference(Path, float)

Returns the area of this path without the other, as Op(Path, PathOp, float) describes.

public Path Difference(Path other, float tolerance = 0.05)

Parameters

other Path

The path to cut away.

tolerance float

How close points must be to count as the same.

Returns

Path

The difference.

Inflate(float, LineJoin, float)

Returns the area grown outwards by a distance, or for a negative distance shrunk inwards: the union of the path and the outline of a pen twice the distance wide, or the difference. Corners grow by the join: round keeps the distance everywhere, miter keeps them sharp up to the miter limit. Open contours count as closed, as fills close them. The grown or cut parts are straight pieces within a hundredth of the distance (at most a tenth of a unit); the rest keeps its curves.

public Path Inflate(float distance, LineJoin join = LineJoin.Round, float miterLimit = 4)

Parameters

distance float

How far to grow; negative to shrink.

join LineJoin

How grown corners look.

miterLimit float

The longest miter, in multiples of twice the distance, as MiterLimit defines it.

Returns

Path

The offset path, simplified; empty when it shrinks away.

Examples

// A frame: one round rectangle cut out of another, kept as curves, so a PDF or SVG backend writes it exactly.
Path frame = Path.RoundRect(new RoundRect(new RectF(0, 0, 200, 140), 24))
    .Difference(Path.RoundRect(new RoundRect(new RectF(20, 20, 160, 100), 12)));

// Many shapes in one pass, applied left to right; overlaps and self-intersections removed; an area grown by 4 units.
Path dots = Path.Combine([Path.Circle(new Vector2(40, 40), 30), Path.Circle(new Vector2(80, 40), 30), Path.Circle(new Vector2(60, 75), 30)], PathOp.Union);
Path clean = Path.Star(new Vector2(50, 50), 45, 20, 5).Simplify();
Path halo = dots.Inflate(4, LineJoin.Round);

Exceptions

ArgumentOutOfRangeException

A value is not finite, join is not defined, or miterLimit is below 1.

Intersect(Path, float)

Returns the area of both this path and the other, as Op(Path, PathOp, float) describes.

public Path Intersect(Path other, float tolerance = 0.05)

Parameters

other Path

The other path.

tolerance float

How close points must be to count as the same.

Returns

Path

The intersection.

Line(Vector2, Vector2)

Creates a straight line, for stroking.

public static Path Line(Vector2 start, Vector2 end)

Parameters

start Vector2

Where it starts.

end Vector2

Where it ends.

Returns

Path

The path.

Op(Path, PathOp, float)

Combines this path's area with another's. Each keeps its own fill rule; open contours count as closed, as fills close them. The result keeps the inputs' lines, quadratics and cubics, split exactly where outlines cross, so it is exact for PDF and SVG output; its outer contours run clockwise on screen and its holes counterclockwise, with the nonzero fill rule, and none of them cross.

public Path Op(Path other, PathOp op, float tolerance = 0.05)

Parameters

other Path

The other path.

op PathOp

The operation.

tolerance float

How close, in path units, points must be to count as the same: crossings are found to well within it, and ends, crossings and curves closer than it merge, which keeps nearly touching or overlapping outlines from making slivers. A caller who will draw the result ten times larger passes a tenth of it.

Returns

Path

The combined path.

Examples

// A frame: one round rectangle cut out of another, kept as curves, so a PDF or SVG backend writes it exactly.
Path frame = Path.RoundRect(new RoundRect(new RectF(0, 0, 200, 140), 24))
    .Difference(Path.RoundRect(new RoundRect(new RectF(20, 20, 160, 100), 12)));

// Many shapes in one pass, applied left to right; overlaps and self-intersections removed; an area grown by 4 units.
Path dots = Path.Combine([Path.Circle(new Vector2(40, 40), 30), Path.Circle(new Vector2(80, 40), 30), Path.Circle(new Vector2(60, 75), 30)], PathOp.Union);
Path clean = Path.Star(new Vector2(50, 50), 45, 20, 5).Simplify();
Path halo = dots.Inflate(4, LineJoin.Round);

Exceptions

ArgumentOutOfRangeException

op is not defined, or tolerance is not positive and finite.

Oval(RectF)

Creates the ellipse that fills a rectangle, clockwise.

public static Path Oval(RectF bounds)

Parameters

bounds RectF

The rectangle.

Returns

Path

The path.

ParseSvg(ReadOnlySpan<char>, FillRule, bool)

Reads SVG path data, the grammar of an SVG d attribute (SVG 1.1 and SVG 2): every command (M, L, H, V, C, S, Q, T, A, Z and their relative forms), implicit repeats, compact numbers such as 0.5.5 and arc flags without separators. Arcs become cubic curves, as ArcTo(Vector2, float, bool, bool, Vector2) makes them. Empty or blank data is an empty path.

public static Path ParseSvg(ReadOnlySpan<char> data, FillRule fillRule = FillRule.NonZero, bool lenient = false)

Parameters

data ReadOnlySpan<char>

The path data.

fillRule FillRule

The fill rule of the path; SVG's fill-rule is not part of the data.

lenient bool

Whether to keep every complete command before the first error instead of throwing, as SVG tells renderers to draw a path with an error in it.

Returns

Path

The path.

Examples

// An icon from SVG path data, drawn four times larger, and written back as compact relative data.
Path heart = Path.ParseSvg("M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09" +
                           "C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z");
string d = heart.Transform(Matrix3x2.CreateScale(4)).ToSvg(new SvgPathFormat { Relative = true, Compact = true });

// Untrusted data: TryParseSvg says whether it is valid; lenient keeps what comes before an error, as browsers draw it.
bool valid = Path.TryParseSvg("M0 0 L10 10 L20", out Path? strict);
Path drawn = Path.ParseSvg("M0 0 L10 10 L20", lenient: true);
try
{
    Path.ParseSvg("M0 0 X");
}
catch (SvgPathException e)
{
    Console.WriteLine($"{e.Problem} at character {e.Position}");
}

Exceptions

SvgPathException

The data breaks the grammar and lenient is false.

ArgumentOutOfRangeException

fillRule is not defined.

Pie(RectF, float, float)

Creates a pie slice: the arc of an ellipse closed through its center.

public static Path Pie(RectF oval, float startDegrees, float sweepDegrees)

Parameters

oval RectF

The rectangle the ellipse fills.

startDegrees float

Where the arc starts.

sweepDegrees float

How far it turns.

Returns

Path

The path.

Polygon(ReadOnlySpan<Vector2>, bool)

Creates a polygon through the points.

public static Path Polygon(ReadOnlySpan<Vector2> points, bool close = true)

Parameters

points ReadOnlySpan<Vector2>

The corners, in order.

close bool

Whether the last point joins the first.

Returns

Path

The path.

Rect(RectF)

Creates a rectangle, clockwise from its top-left corner.

public static Path Rect(RectF rect)

Parameters

rect RectF

The rectangle.

Returns

Path

The path.

Examples

canvas.FillRect(new RectF(10, 10, 80, 40), Rgba8.Red);
canvas.FillRoundRect(new RoundRect(new RectF(100, 10, 80, 40), 12), new Rgba8(0, 128, 0, 255));

// Each corner can have its own radii; radii that would overlap are scaled down together, as CSS does.
CornerRadii tab = new(topLeft: 16, topRight: 16, bottomRight: 0, bottomLeft: 0);
canvas.FillRoundRect(new RoundRect(new RectF(190, 10, 100, 40), tab), Rgba8.Black);

canvas.FillOval(new RectF(10, 60, 120, 60), new Rgba8(255, 200, 0, 255));
canvas.FillCircle(new Vector2(170, 90), 30, new Rgba8(80, 80, 200, 255));
canvas.FillArc(new RectF(210, 60, 60, 60), startDegrees: -90, sweepDegrees: 120, Rgba8.Red);
canvas.FillRing(new RoundRect(new RectF(10, 130, 60, 60), 30), new RoundRect(new RectF(25, 145, 30, 30), 15), Rgba8.Black);

// The same shapes as paths, to build on or to reuse.
Path hexagon = Path.RegularPolygon(new Vector2(120, 160), 30, sides: 6);
Path star = Path.Star(new Vector2(200, 160), outerRadius: 30, innerRadius: 12, points: 5);
canvas.FillPath(hexagon, new Rgba8(0, 150, 150, 255));
canvas.FillPath(star, new Rgba8(220, 160, 0, 255));

RegularPolygon(Vector2, float, int, float)

Creates a regular polygon with its first corner straight up from the center, turned clockwise by rotationDegrees.

public static Path RegularPolygon(Vector2 center, float radius, int sides, float rotationDegrees = 0)

Parameters

center Vector2

The center.

radius float

The distance from the center to each corner.

sides int

The number of sides, at least 3.

rotationDegrees float

The clockwise turn.

Returns

Path

The path.

Exceptions

ArgumentOutOfRangeException

sides is below 3.

Reverse()

Returns the path with each contour running the other way; contours keep their order and closed state.

public Path Reverse()

Returns

Path

The reversed path.

Ring(RoundRect, RoundRect)

Creates the ring between two round rectangles: outer clockwise and inner counterclockwise, with EvenOdd, which is what Skia's drawDRRect draws and what PDF writes directly. The inner shape should lie inside the outer one.

public static Path Ring(RoundRect outer, RoundRect inner)

Parameters

outer RoundRect

The outer edge.

inner RoundRect

The inner edge: the hole.

Returns

Path

The path.

RoundRect(RoundRect)

Creates a round rectangle, clockwise.

public static Path RoundRect(RoundRect roundRect)

Parameters

roundRect RoundRect

The round rectangle.

Returns

Path

The path.

Simplify(float)

Returns the same area as this path, by its fill rule, as contours that neither overlap nor cross themselves or each other: outer contours clockwise on screen, holes counterclockwise, with the nonzero fill rule, curves kept as Op(Path, PathOp, float) keeps them.

public Path Simplify(float tolerance = 0.05)

Parameters

tolerance float

How close points must be to count as the same, as Op(Path, PathOp, float) describes.

Returns

Path

The simplified path.

Examples

// A frame: one round rectangle cut out of another, kept as curves, so a PDF or SVG backend writes it exactly.
Path frame = Path.RoundRect(new RoundRect(new RectF(0, 0, 200, 140), 24))
    .Difference(Path.RoundRect(new RoundRect(new RectF(20, 20, 160, 100), 12)));

// Many shapes in one pass, applied left to right; overlaps and self-intersections removed; an area grown by 4 units.
Path dots = Path.Combine([Path.Circle(new Vector2(40, 40), 30), Path.Circle(new Vector2(80, 40), 30), Path.Circle(new Vector2(60, 75), 30)], PathOp.Union);
Path clean = Path.Star(new Vector2(50, 50), 45, 20, 5).Simplify();
Path halo = dots.Inflate(4, LineJoin.Round);

Exceptions

ArgumentOutOfRangeException

tolerance is not positive and finite.

Star(Vector2, float, float, int, float)

Creates a star whose points lie on outerRadius and whose inner corners lie on innerRadius, the first point straight up, turned clockwise by rotationDegrees.

public static Path Star(Vector2 center, float outerRadius, float innerRadius, int points, float rotationDegrees = 0)

Parameters

center Vector2

The center.

outerRadius float

The distance to each point.

innerRadius float

The distance to each inner corner.

points int

The number of points, at least 2.

rotationDegrees float

The clockwise turn.

Returns

Path

The path.

Exceptions

ArgumentOutOfRangeException

points is below 2.

ToString()

Returns a string that represents the current object.

public override string ToString()

Returns

string

A string that represents the current object.

ToSvg(SvgPathFormat?)

Writes the path as SVG path data, for a d attribute. Lines, quadratics and cubics are written as L, Q and C (H and V for straight lines along an axis when compact); arcs were cubics already. The fill rule is not part of path data.

public string ToSvg(SvgPathFormat? format = null)

Parameters

format SvgPathFormat

How to write it; null for absolute commands with three decimals.

Returns

string

The path data.

Examples

// An icon from SVG path data, drawn four times larger, and written back as compact relative data.
Path heart = Path.ParseSvg("M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09" +
                           "C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z");
string d = heart.Transform(Matrix3x2.CreateScale(4)).ToSvg(new SvgPathFormat { Relative = true, Compact = true });

// Untrusted data: TryParseSvg says whether it is valid; lenient keeps what comes before an error, as browsers draw it.
bool valid = Path.TryParseSvg("M0 0 L10 10 L20", out Path? strict);
Path drawn = Path.ParseSvg("M0 0 L10 10 L20", lenient: true);
try
{
    Path.ParseSvg("M0 0 X");
}
catch (SvgPathException e)
{
    Console.WriteLine($"{e.Problem} at character {e.Position}");
}

Transform(Homography)

Returns the path under a perspective transform. Lines stay lines; curves are split until each piece's projected cubic stays within a hundredth of a unit of the true projection, since a projected Bézier curve is no longer polynomial.

public Path Transform(Homography homography)

Parameters

homography Homography

The transform.

Returns

Path

The transformed path.

Exceptions

ArgumentException

A point maps to infinity or behind the viewer.

Transform(Matrix3x2)

Returns the path with every point transformed; the fill rule is kept.

public Path Transform(Matrix3x2 matrix)

Parameters

matrix Matrix3x2

The affine transform.

Returns

Path

The transformed path.

TryParseSvg(ReadOnlySpan<char>, out Path?)

Reads SVG path data as ParseSvg(ReadOnlySpan<char>, FillRule, bool) does, returning whether it was valid instead of throwing.

public static bool TryParseSvg(ReadOnlySpan<char> data, out Path? path)

Parameters

data ReadOnlySpan<char>

The path data.

path Path

The path, with the nonzero fill rule, or null when the data breaks the grammar.

Returns

bool

true if the data was valid.

Examples

// An icon from SVG path data, drawn four times larger, and written back as compact relative data.
Path heart = Path.ParseSvg("M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09" +
                           "C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z");
string d = heart.Transform(Matrix3x2.CreateScale(4)).ToSvg(new SvgPathFormat { Relative = true, Compact = true });

// Untrusted data: TryParseSvg says whether it is valid; lenient keeps what comes before an error, as browsers draw it.
bool valid = Path.TryParseSvg("M0 0 L10 10 L20", out Path? strict);
Path drawn = Path.ParseSvg("M0 0 L10 10 L20", lenient: true);
try
{
    Path.ParseSvg("M0 0 X");
}
catch (SvgPathException e)
{
    Console.WriteLine($"{e.Problem} at character {e.Position}");
}

Union(Path, float)

Returns the area of this path or the other, as Op(Path, PathOp, float) describes.

public Path Union(Path other, float tolerance = 0.05)

Parameters

other Path

The other path.

tolerance float

How close points must be to count as the same.

Returns

Path

The union.

WithFillRule(FillRule)

Returns the same outline with another fill rule.

public Path WithFillRule(FillRule fillRule)

Parameters

fillRule FillRule

The fill rule.

Returns

Path

The path; this one when the rule is the same.

Exceptions

ArgumentOutOfRangeException

fillRule is not defined.

Xor(Path, float)

Returns the area of exactly one of this path and the other, as Op(Path, PathOp, float) describes.

public Path Xor(Path other, float tolerance = 0.05)

Parameters

other Path

The other path.

tolerance float

How close points must be to count as the same.

Returns

Path

The exclusive or.