Skip to main content
Technical preview

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:

The Ghostscript Tiger: 138 path fills re-binned every frameOpen in editor →

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);
A stroked path with a gradient along the arc lengthOpen in editor →

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. dst is a rect, either a Float32Array or { x?, y?, width, height } (x and y default 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.