Style
Shape.style holds everything that decides how a shape looks, apart from its geometry. It is plain readonly data. The package stores it, parses it and copies it with the shape. It never draws it. The planned @draw/sketch package turns it into strokes.
type Style = { readonly stroke: Stroke; readonly fill: Fill; readonly roughness: number; readonly opacity: number; readonly seed: Seed;};| Field | Meaning |
|---|---|
stroke |
The outline: a color, a width above 0 and a style of StrokeStyle |
fill |
{ kind: "none" }, or a kind of "hachure", "cross-hatch" or "solid" with a color |
roughness |
How far the hand-drawn wobble strays. 0 draws straight, 1 is the default |
opacity |
0 to 1, for the whole shape. Colours carry no alpha of their own |
seed |
The integer that fixes the wobble, so a shape looks the same on every draw |
StrokeStyle is "solid", "dashed" or "dotted".
One spelling for each fill
Fill is a union. “No fill” is { kind: "none" }, and a fill of any other kind always has a colour. Excalidraw keeps a background colour and a fill style as two fields, so “transparent with hachure” and “no fill with a colour” both exist and mean the same thing. Here neither state can be written.
Colour, seed and the constructors
Color is a branded string of the form #rrggbb, six hex digits in any case. Opacity is a separate field. Seed is a branded integer from 0 to 2^32 - 1.
color(value) and seed(value) brand a value you already trust, as shapeId does. They do not check it. Untrusted input goes through parseShape, which does.
The package has no seed generator, so createShapeId stays its only impure function. The app draws the seed, for example from crypto.getRandomValues, and replaces the seed of a new shape with it.
Defaults
DEFAULT_STYLE is Excalidraw’s default: a solid #1e1e1e stroke of width 2, no fill, roughness 1, opacity 1 and seed 0. Every shape built from it has the same wobble, so callers replace the seed for each shape they create.
What is not here
Style that only one kind of shape has lives on that shape’s drawable, so a rectangle cannot carry an arrowhead or a font.
Picking
A shape counts as filled for picking when style.fill.kind is not "none". The colour does not matter.
import { DEFAULT_STYLE, color, indexAbove, pagePoint, pickShape, radians, seed, shapeId,} from "@draw/shapes";import type { Shape, Style } from "@draw/shapes";
const style: Style = { ...DEFAULT_STYLE, stroke: { color: color("#e03131"), width: 4, style: "dashed" }, fill: { kind: "hachure", color: color("#ffc9c9") }, seed: seed(1337),};
const card: Shape = { id: shapeId("card"), frame: { x: 0, y: 0, width: 200, height: 200, rotation: radians(0) }, drawable: { kind: "rectangle", cornerRadius: 0 }, index: indexAbove([]), style, groupIds: [],};
const badge: Shape = { id: shapeId("badge"), frame: { x: 80, y: 80, width: 40, height: 40, rotation: radians(0) }, drawable: { kind: "rectangle", cornerRadius: 0 }, index: indexAbove([card]), style: DEFAULT_STYLE, groupIds: [],};
const middle = pagePoint(100, 100);
export const withHachure: string | undefined = pickShape([card, badge], middle, { tolerance: 4,})?.id;
export const withoutFill: string | undefined = pickShape( [{ ...card, style: DEFAULT_STYLE }, badge], middle, { tolerance: 4 },)?.id;The hachure-filled card is picked by a click in its middle, even though a smaller badge lies above it. The badge is hollow, and a filled shape that is hit hides hollow shapes above it. With DEFAULT_STYLE the card is hollow too, and the smaller badge wins.