Rough edges
roughEdges(points, options) returns the Path of a straight-edged outline drawn by hand. Use it for outlines whose corners should stay sharp: a rectangle, a diamond, an arrowhead or a line’s polyline. Smooth outlines belong to roughCurve.
type RoughEdgesOptions = { readonly closed: boolean; readonly roughness: number; readonly seed: Seed; readonly passes?: 1 | 2;};| Option | Meaning |
|---|---|
closed |
true joins the last point back to the first, as a rectangle needs |
roughness |
Scales every random offset. 0 draws the input exactly, 1 is the default look |
seed |
Fixes every random choice, so the same input and seed give the same path |
passes |
2 (the default) draws each edge twice. 1 draws only the first pass |
What each edge becomes
Each edge is drawn twice. Both passes are a move and a curve whose control points sit 20 to 40 percent along the edge. They differ only in their random offsets, and the second pass uses half the offset. The two passes make the doubled, slightly crooked line of a hand-drawn stroke.
The formula follows roughjs:
- Offset. A control point moves by up to 2 units in each axis, times
roughness. An edge shorter than 20 units uses a tenth of its length instead, so a short edge never swings wider than itself. - Length gain. The offset is damped on long edges. It is full below 200 units, falls linearly from 0.9 at 200 to 0.4 at 500, and stays at 0.4 beyond.
- Bow. Both control points shift sideways by a random amount that grows with the edge’s length, so an edge bows instead of running dead straight.
- Overshoot. The two passes start and end at the vertices while
roughnessis below 2. From 2 up, the ends move as well, so the passes overshoot the corner.
One pass
A dashed or dotted stroke uses passes: 1. Two passes would put two dash patterns on top of each other, and they do not line up, so the dashes would blur. This matches Excalidraw, which turns off roughjs’s second stroke for any style other than solid. The single pass is the same first pass that passes: 2 draws.
Roughness 0
A roughness of 0 or less skips the wobble. The result is one continuous path of line commands through the input points, with a closing line back to the first point when closed is set. The output is exactly the input geometry.
Degenerate input
Fewer than two points give an empty path. Two equal points give an edge of length 0, which has no offset and no NaN.
import { localPoint, seed } from "@draw/shapes";import { roughEdges } from "@draw/sketch";import type { Path } from "@draw/sketch";
const corners = [localPoint(0, 0), localPoint(100, 0), localPoint(100, 60), localPoint(0, 60)];
export const sketched: Path = roughEdges(corners, { closed: true, roughness: 1, seed: seed(7) });
export const straight: Path = roughEdges(corners, { closed: true, roughness: 0, seed: seed(7) });sketched is sixteen commands, a move and a curve for each pass over each of four edges, and it is the same on every call. straight is a move and four line commands that return to the start.