stroke-outline
Turns a vector path into the outline of its stroke, the shape a canvas paints
for
stroke(), as a path you can fill, cache, hit-test or tessellate.
Curves stay curves. Written in C++20, and also built for WebAssembly as an npm
package.
Gray with a red edge: the outline the library computed.
Dashed: the input path.
Try the star with an inside stroke wider than its arms (around 40), with and
without clipToShape.
Features
- Lines, quadratic and cubic Béziers and SVG arcs in; curves out, never flattened.
- Miter, bevel and round joins with a miter limit; butt, square and round caps.
- Dash patterns with an offset; zero-length dashes draw dots.
- Inside and outside strokes, relative to the whole shape's fill, holes included.
-
clipToShapeturns strokes wider than the shape, or overlapping each other, into clean outlines. - SVG path data in and out, with parse errors reported.
- Same results at any scale.
JavaScript
npm install stroke-outline
import { init } from 'stroke-outline';
const so = await init(); // loads the WebAssembly module; do it once
// stroke SVG path data; the outline is a Path
const outline = so.buildOutline('M 10 10 H 90 A 20 20 0 0 1 90 50', { lineWidth: 8, lineCap: 'round' });
ctx.fill(new Path2D(outline.toSVG()));
outline.commands; // [{ type: 'moveTo', x, y }, { type: 'cubicTo', ... }, ...] for WebGL, hit testing...
outline.error; // null, or { message: 'expected a number at position 11' } for malformed data
// parse SVG once, then stroke it as often as you like
const icon = so.pathFromSVG('M 0 0 H 100 V 100 H 0 Z M 30 30 V 70 H 70 V 30 Z');
const thin = so.buildOutline(icon, { lineWidth: 2 });
const thick = so.buildOutline(icon, { lineWidth: 12, align: 'inside' });
thinEl.setAttribute('d', thin.toSVG());
thickEl.setAttribute('d', thick.toSVG());
// or build the path in code
const path = new so.Path().moveTo(0, 0).lineTo(100, 0).quadTo(150, 50, 100, 100);
const dashed = so.buildOutline(path, { lineWidth: 6, dash: [12, 6] });
ctx.fill(new Path2D(dashed.toSVG())); // each dash is its own subpath
On npm as stroke-outline, with TypeScript types. It works in browsers, workers and Node 20+ with no bundler setup: the WebAssembly is embedded in the package.
C++
find_package(stroke_outline 0.1 REQUIRED)
target_link_libraries(app PRIVATE stroke_outline::stroke_outline)
#include "stroke_outline/stroke_outline.hpp"
using namespace stroke_outline;
StrokeSettings settings;
settings.line_width = 8.0;
settings.line_cap = LineCap::Round;
// stroke SVG path data; the outline is a Path
std::optional<SvgParseError> error; // optional: malformed data is stroked up to the error
Path outline = build_outline("M 10 10 H 90 A 20 20 0 0 1 90 50", settings, error);
std::string d = outline.to_svg();
outline.for_each({ .move_to = [](double x, double y) { /* ... */ }, /* line_to, cubic_to... */ });
if (error) std::cerr << error->message; // e.g. "expected a number at position 11"
// parse SVG once, then stroke it as often as you like
Path icon = Path::from_svg("M 0 0 H 100 V 100 H 0 Z M 30 30 V 70 H 70 V 30 Z");
Path thin = build_outline(icon, { .line_width = 2.0 });
Path thick = build_outline(icon, { .line_width = 12.0, .align = StrokeAlign::Inside });
// or build the path in code
Path path = Path {}.move_to(0, 0).line_to(100, 0).quad_to(150, 50, 100, 100);
settings.dash = DashSettings { { 12.0, 6.0 } };
Path dashed = build_outline(path, settings);
Options
All optional. In C++ they are StrokeSettings fields in snake_case.
| Option | Default | |
|---|---|---|
lineWidth |
1 |
Must be positive and finite, otherwise nothing is stroked. |
lineJoin |
'miter' |
'miter', 'bevel', 'round' |
lineCap |
'butt' |
'butt', 'square', 'round' |
miterLimit |
4 |
Longest miter, as a multiple of the width; sharper corners are beveled. |
align |
'center' |
'center', 'inside', 'outside'.
Applies to closed subpaths.
|
fillRule |
'nonzero' |
'nonzero', 'evenodd': how the shape is filled,
which decides inside and outside.
|
clipToShape |
false |
Resolve overlaps into simple contours. Slower, so off by default. |
dash, dashOffset |
none, 0 |
For example [10, 5]. [0, 10] with round caps
draws dots.
|
Fill outlines with the nonzero rule (the default for canvas and
SVG): they can overlap themselves. With clipToShape, closed shapes
come out as simple contours that fill the same under even-odd.