Skip to main content

DrawdyElementSchema

The elements you add to the canvas with scene:add-drawdy-elements and preview commands. All variants share a common base, which carries the full set of visual style overrides, an optional groupId — elements that share one select, move, resize and rotate together — and an optional frameId, the frame the element belongs to:
Every ElementStyle field is optional and maps 1:1 onto the canvas element. Unset fields fall back to per-variant defaults (e.g. shapes get roughness: 1 and cornerRadius: 12, lines and arrows get roughness: 0; seed is randomized). On line/arrow, strokeColor takes precedence over the variant’s required color, and strokeWidth falls back to lineStrokeWidth. To match the user’s theme, read colors from ModuleStyling, for example styling.tokens["--dd-text-primary"] for connectors.
  • A frame whose meta has hieFrameLabel: true has no name chip above its top-left corner: Drawdy neither draws it nor treats it as a target for selecting or renaming the frame. Use it when your driver draws its own label for the frame.
  • frame.name is the label shown above the frame. Leave it out, or pass an empty string, and the frame is numbered after the frames already on the board (Frame 1, Frame 2, …).
  • shape.roughness: 0 is crisp, higher is sketchier. text is a label rendered inside the shape.
  • A shape with componentType: "sticky-note" is a taped paper note, the same element the sticky note tool creates. It defaults to 200 × 193 at (x, y), 16 px text aligned top-left. fillColor picks the paper: a palette name ("blue", "lime", "orange", "teal", "purple", "pink", "neutral") or any #rrggbb hex, which gets a paper derived from it; anything else falls back to blue. The tape colour and text colour come from the paper, so strokeColor and textColor are ignored.
  • line.bend curves the line through one control point (quadratic) or several (through every bend, in order). arrow.bend curves the shaft through a single control point.
  • Each end of a line or arrow is set either by an explicit coordinate (from / to) or by a LineBinding (startBinding / endBinding) — the two are mutually exclusive per end, so you can pin one end and bind the other. A LineBinding attaches that end to elementId at the normalized anchor (anchorX, anchorY, each 0–1 across the target’s bounding box); the end then follows that element as it moves. Bindings resolve against elements already on the canvas and elements created earlier in the same add-drawdy-elements batch.
  • elbowRouting routes the line or arrow orthogonally between its two ends instead of straight; bend is ignored when it is set.
  • image.isEditable: false locks the image’s content: the image toolbar (replace, crop, download) is hidden and crop mode can’t be entered by double-click, Enter or ⌘/Ctrl-dragging a handle. The image can still be moved, resized and deleted. It defaults to true and can only be set when the image is added.
  • text.width/height are optional and can be measured later.
  • component embeds a live DomElementSchema UI tree at (x, y) on the canvas — the same declarative schema used for floating elements, but committed as a scene element that pans and zooms with the board.
  • A component schema’s root width or height given in % is resolved against the element’s width/height when the element is created, so [100, "%"] on a 640 × 480 component becomes [640, "px"] × [480, "px"]. Only the root is resolved, and the resolved size does not follow later resizes.

DomElementSchema

The declarative UI tree for floating elements. Layout primitives nest via children; leaves carry a child.
  • image.child is the image URL; SVG works.
  • iframe.child is either { src }, a URL to load, or { srcDoc }, an HTML string rendered in place. The iframe is sandboxed with allow-scripts only: pages run in an opaque origin, so they cannot reach Drawdy’s DOM or storage, and pages that rely on cookies or localStorage may not work. A src site must allow framing (no X-Frame-Options: DENY or restrictive frame-ancestors).
  • iframe.pointerLock: true is for mouse look in games and 3D views. The sandbox then also allows pointer lock, so the page can call requestPointerLock() from a click inside it; Escape always releases it. Drawdy draws a 28 px drag bar at the top of the iframe, showing label, because the board can’t drag an element through an iframe that takes the pointer; the page gets the iframe’s height minus the bar. label also names the iframe for screen readers.
For image, child is the image URL and may be an SVG string. domId gives an element a handle you can target with DOM commands and click subscriptions. A ring or other circular shape is just a box with borderRadius: [50, "%"] — no dedicated type. backgroundColor accepts a CSS gradient (e.g. radial-gradient(circle, ...)), so a soft glowing disc is a single box. Create it with command:dom:create-floating-element and asPopover: false to render it at its exact position (rather than as a collision-aware popover). pointerEvents then decides interactivity: "none" makes it a click-through decoration that follows the cursor without blocking the canvas; "auto" makes it grabbable, so subscription:dom:drag scoped to its id reports drags on it.

Element properties

The property set you can read back from the scene and update on it. It includes every ElementStyle field, so a driver can read an element’s look and reproduce it.
type describes the element as it is stored in the scene, which is not the same as the DrawdyElementSchema type you create it with: a shape, line or arrow reads back as type: "path". componentType is only reported for the shapes you can create through the protocol: a path whose shape is "rect", "circle", "diamond" or "sticky-note". For every other element it is absent, even when you request it. That includes lines, arrows, other shapes drawn in Drawdy (such as triangles or stars), and components. A path without componentType is a line, an arrow, or one of those other shapes, and the protocol does not currently say which. Because the type is a union on type, checking type === "path" narrows componentType to ShapeComponentType. Every property is still optional in a SubscribedDrawdyElement, so request both type and componentType if you need to branch on the shape. When you query or subscribe you pass a list of SubscribeableKey; each returned SubscribedDrawdyElement carries id plus exactly the properties you asked for. Style fields are only present when the element has them set, groupId only when the element belongs to a group, and frameId only when it belongs to a frame. frameName is present only on frames; a frame with no stored name reads back as Frame. startBinding/endBinding are present only on lines and arrows whose end is bound to another element — request them to discover which elements a connector links. The package is type-only, so to request every style field build the key list yourself — a Record<keyof ElementStyle, 0> literal checked with satisfies gives you a compile-time-complete list to Object.keys. Updates are a partial patch:
meta merges into existing meta. Style fields are set as given, so a driver can restyle existing elements (recolor, change stroke width, switch fillStyle) with command:scene:update-drawdy-elements. localAnimation attaches (or, when null, clears) a declarative animation on the element. groupId joins the element to a group — any string unique within the board, or an existing group’s id to join it — and null ungroups it. frameId moves the element into the frame with that id, and null takes it out of its frame. frameName renames a frame; an empty string names it Frame, the same as clearing the name in the editor. Setting it on anything other than a frame fails the command with bad-argument. transform moves the element so the top-left of its bounding rect lands on x/y, the same values you read back as x and y; give only one to move along that axis. Only the element itself moves: a frame’s children stay where they are, so move them in the same update. Values must be finite, or the command fails with bad-argument. width and height resize the element so its bounding rect reads back with that size. The top-left stays where it is, or moves to transform’s x/y when you send both in the same update. Give only one to resize along that axis. How the element changes depends on its type:
  • Shapes, lines, arrows, freedraw and frames are stretched to fit. A freedraw’s stroke width is not scaled; it is included in the size you read back.
  • Images and components change their scale, so their content is stretched, not reflowed.
  • Text sets its wrap width, so the text reflows and its height changes with the number of lines. height sets the height of the text box; a value smaller than the text needs has no effect.
A frame’s children are not resized with it. Values must be finite and greater than 0. An axis with no size, such as the height of a horizontal line, cannot be resized. Both cases fail the command with bad-argument. To rotate an element, use localAnimation, which transforms about the element’s own center.

LocalAnimation

A declarative, purely local animation. Setting it makes the element animate; the animation is not committed to the scene — the animation is synced to collaborators as it’s simply a property on an element, only its presence on the element does. Clear it with null.
  • time.durationMs is the length of one cycle. time.curve eases the time progress before it samples the tracks. time.repeat is "none" (play once, then hold the final frame) or "ping-pong" (bounce 0 → 1 → 0 forever).
  • Each transform field is a keyframe track — an array of values sampled across the (eased) progress. x/y translate in canvas units; width/height are scale multipliers about the element’s center; rotation is radians about its center; opacity multiplies its alpha. Omitted fields hold their rest value.
  • strokeColor/textColor/fillColor are color tracks. Unlike the numeric tracks they are not offsets from the rest value: the sampled color replaces the element’s own color for as long as the animation is attached, and the element’s stored style is untouched. components is a keyframe list of [r, g, b] (each 0–255) for "rgb", or [l, c, h] (lightness 0–1, chroma, hue in degrees) for "oklch". Channels interpolate independently using animation.curve; an oklch hue always takes the shorter way around the wheel, so 350 → 10 passes through 0 rather than sweeping through 180. A single keyframe holds a constant color.
  • animation.curve is how keyframes interpolate: "linear" between values, or "catmull" for a smooth spline through them.

ModuleStyling

The theme handed to activate and re-delivered on subscription:dom:theme-changed. Each field is a resolved CSS value string for the current mode.
For example, { theme: "light", background: "#fff", radiusSm: "4px", tokens: { "--dd-surface-panel": "#ffffff", ... } }.

Theme tokens

tokens is the set of --dd-* design tokens that make extension UI match Drawdy. It is keyed by the CSS custom property name, so you can apply every token to a document in one loop, for example in a webview after your driver sends tokens to it:
The tokens are a guide, not a requirement: use var(--dd-*) where you want your UI to match Drawdy, and use your own values where your extension needs its own look. Apply the tokens again on theme-changed, because color values differ between Light and Dark. Type, spacing, size and radius tokens are the same in both modes. For accent-colored text on a panel, --dd-accent-text has enough contrast in both modes; --dd-accent is meant as a fill and is hard to read as text in Light mode. Type scale: Drawdy’s own panels use 11 to 12px text and show hierarchy with text color (primary, secondary, tertiary) rather than size. The other ModuleStyling fields are deprecated. They predate tokens and are kept so existing drivers keep working; use tokens in new code. Each deprecated field names its replacement token in its type. Two values differ: ring is the lime focus color, while --dd-focus-ring is neutral gray, and surface2 is the panel section color, while --dd-surface-section is one neutral step further from the panel color, so it stays visible inside a panel in Dark mode.

ToolStyleState

The style state of a built-in tool, returned by scene:query-tool-style-state.
StyleableToolId is the subset of ToolId that ToolStyleState covers.

PreviewTransform

A pose delta from an element’s rest pose, used by scene:preview-transforms.
x/y are a world-space translation; rotation (radians) is applied on top of the element’s rest rotation about its bounding-box center. scale is reserved and currently ignored.

CSS units

Dimension values pair a number with a CSSUnit:
The full CSSFontUnit, CSSViewportUnit, CSSFrequencyUnit, and CSSResolutionUnit unions are exported as well; see css-unit.ts in the package.