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