Redraw is currently in technical preview, available to wcandillon.dev subscribers. API is unstable.
Paths
canvas.drawPath(path, paint, options?) draws a Path. The paint
picks the mode: with a stroke set, the path is stroked; without one,
it is filled.
Under the hood a path is not one command but one command per covered screen tile: the CPU bins the path's segments into the pipeline's tile grid so each pixel only measures nearby segments. Boundary tiles walk the path for winding, interior tiles become full-coverage commands, and outside tiles are culled.
Building paths
You can build a path from SVG data:
import { parseSVG } from "redraw";
const path = parseSVG("M 10 10 L 90 90 C 100 50, 50 50, 10 10 Z");
or with the path builder API:
import { PathBuilder, vec } from "redraw";
const cx = 100, cy = 100, size = 80;
const heart = new PathBuilder();
heart.moveTo(vec(cx, cy - size * 0.4));
heart.cubicTo(
vec(cx + size * 0.5, cy - size * 0.8),
vec(cx + size, cy - size * 0.2),
vec(cx, cy + size * 0.6),
);
heart.cubicTo(
vec(cx - size, cy - size * 0.2),
vec(cx - size * 0.5, cy - size * 0.8),
vec(cx, cy - size * 0.4),
);
heart.close();
// Paths are immutable
const path = heart.makePath();
PathBuilder is the imperative API: moveTo, lineTo, quadTo,
cubicTo, close. Use close() when you want a closed contour; omit
it for open ones. close() also starts a fresh contour, so chaining
close().moveTo(...) works as expected for multi-contour paths.
Filling
A paint without a stroke fills the path by winding number. Open
contours are implicitly closed. The fillRule option selects the rule,
"nonzero" (default) or "evenodd":
import { Paint, parseSVG } from "redraw";
const paint = new Paint().setColor("#e8a33d");
canvas.drawPath(path, paint);
canvas.drawPath(ring, paint, { fillRule: "evenodd" });
The tiger below is 138 SVG paths drawn as fills, re-recorded every frame under an animated zoom:
Stroking
A paint with a stroke bands the path's distance field with the width
your stroke function returns, so the width can vary along the path
(read ctx.t for the arc-length position, ctx.tan for the tangent):
import { Paint, fitPath, strokeWidth } from "redraw";
const HelloStroke = strokeWidth(
(_ctx, _tctx, props) => {
"use gpu";
return props.width;
},
{ width: 0 },
{ maxStrokeWidth: 25 },
);
const paint = new Paint()
.addShader(PathGradient, { shift: 0 })
.setStroke(HelloStroke, { width: 25 });
canvas.drawPath(pathGeo, paint);
See Custom Effects: Stroke for width functions (tapers, calligraphy pens, animated heads).
Stroke grouping
In stroke mode, each group of segments composites as its own unit, and
crossing groups composite in path order: translucent strokes darken
where independent strands overlap, while self-intersections within a
group blend as one coverage. The grouping option decides what joins a
group:
"contour"(default): each contour is its own group."strand": follows pen continuity across contours; closed seams and pen-down contour joins continue the same strand.
canvas.drawPath(path, strokePaint, { grouping: "strand" });
Animating a reveal
path.segment(t0, t1) returns the sub-path between two normalized
arc-length parameters. Since the canvas is immediate mode, a draw-in
effect is just a different segment each frame:
export function render(canvas: Canvas, { width, height, time }: FrameInfo) {
canvas.fill(background);
// Draw on over 4 seconds (time is in milliseconds).
const progress = Math.min(time / 4000, 1);
canvas.drawPath(pathGeo.segment(0, progress), paint);
}
Path helpers
Available on any Path instance:
path.bounds(): axis-aligned bounding box.path.segment(t0, t1): sub-path between two arc-length parameters in[0, 1].path.splitContours(): array of single-contour paths, useful when an SVG has multiple disconnected sub-paths you want rendered independently.path.fit(mode, src, dst): explicit fit ("contain" / "cover" / "fill").
And as module-level functions:
fitPath(svgStringOrPath, dst, fit = "contain"): parse-and-fit in one call.dstis a rect, either aFloat32Arrayor{ x?, y?, width, height }(xandydefault to 0).deflate(rect, amount): insets a rect on all sides, e.g.deflate({ width, height }, 100)for a padded canvas area.capWidth(rect, maxWidth): limits a rect's width, keeping it centered.fitbox(mode, src, dst): returns the fit matrix without applying it.