Skip to content

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 roughness is 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.

docs/examples/rough-rectangle.ts
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.