Skip to content

Paint

paintShape(shape) is the one call a renderer makes per shape. It reads the shape’s drawable, frame and style, picks the right sketch functions, and returns a Drawing: the operations to replay, in order, bottom first.

type DrawOp =
| {
readonly kind: "stroke";
readonly path: Path;
readonly color: Color;
readonly width: number;
readonly opacity: number;
readonly dash?: readonly number[];
}
| {
readonly kind: "fill";
readonly path: Path;
readonly color: Color;
readonly opacity: number;
};
type Drawing = readonly DrawOp[];

DrawOpKind names the two operation kinds, so a renderer writes op.kind === DrawOpKind.fill. See named values.

Every path is in frame-local units, like the rest of the package. The renderer applies the frame’s translation and rotation once, then replays each operation. A stroke draws its path with the given width and optional dash pattern. A fill fills its path.

docs/examples/paint-rectangle.ts
import { DEFAULT_STYLE, color, indexAbove, radians, seed, shapeId } from "@draw/shapes";
import type { Shape } from "@draw/shapes";
import { paintShape } from "@draw/sketch";
import type { Drawing } from "@draw/sketch";
const card: Shape = {
id: shapeId("card"),
frame: { x: 40, y: 40, width: 100, height: 60, rotation: radians(0) },
drawable: { kind: "rectangle", cornerRadius: 0 },
index: indexAbove([]),
style: {
...DEFAULT_STYLE,
seed: seed(7),
fill: { kind: "hachure", color: color("#ffc9c9") },
},
groupIds: [],
};
export const drawing: Drawing = paintShape(card);

What each drawable becomes

Drawable Operations
rectangle, diamond The fill, if any, then the outline from roughEdges
line The outline from roughEdges. A line is open, so its fill is ignored
ellipse The fill, hatched through the curve’s base, then the outline from roughCurve
arrow A straight shaft from roughEdges, as one rough line like Excalidraw’s. A bent shaft from roughCurve. Then one head per end, see Arrowheads below
freehand One fill of the inkOutline polygon, in the stroke colour
text Nothing. The canvas draws text with its own font

Operations with an empty path are dropped, so a zero-size shape paints no fill.

Fills

A solid fill is one fill operation of the outline. A hachure or cross-hatch fill is a stroke operation from hachureFill, in the fill colour, half the stroke width, with a gap of four stroke widths between lines. The fill uses a seed derived from the shape’s seed, so it does not repeat the outline’s wobble.

Dashes

A solid stroke draws two passes and has no dash. A dashed stroke has the pattern [8, 8 + width] and a dotted one [1.5, 6 + width]. Both draw one pass and are half a unit wider, as in Excalidraw, because two passes would blur the dashes.

Arrowheads

Each end of an arrow paints its startHead or endHead. The geometry follows Excalidraw’s getArrowheadPoints.

Head Size Angle Operations
arrow 25 20° One stroke from one wing through the tip to the other wing
bar 15 90° The same stroke. At 90 degrees the wings sit across the tip, so it draws a bar
triangle 15 25° A fill of the tip and both wings in the stroke colour, then its outline
dot 15 A fill of a 16-point ring in the stroke colour, then its outline. Diameter is size + stroke width - 2
none Nothing

The size is capped at half the arrow’s chord, so a short arrow gets a small head. The wings are the point size units back from the tip along the arrow’s direction, rotated by plus and minus the angle around the tip. The direction runs from a point 15 percent of the way back along the shaft to the tip, so a bent arrow’s head follows the curve near its end. Excalidraw takes the same direction from its last Bézier segment.

A head is at most roughness 1, and a dot at most 0.5. Each end uses a seed derived from the shape’s seed. On a dashed arrow the heads are solid. On a dotted arrow, an arrow or bar head stays dotted with a gap 2 units tighter, so it stays legible. A zero-length arrow has no direction and paints no heads.

Freehand pressure

When every sample has a pressure of exactly 0.5, the input came from a mouse, which always reports 0.5, so paintShape asks inkOutline to simulate pressure from speed. Any other pressure is real pen pressure, even when it stays constant. Excalidraw uses the same rule.