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

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.