draw
Draw on a doodle board. The user sees each call appear live in the editor.
## Doodle ops
A doodle is a board (default 1280 x 720 px, origin top-left, y grows down). Send ops as
`{"ops": [...]}`; they apply in order, all or nothing. The answer lists the ids each op
created, in order. Address elements by id; read the board for the ids it already has.
**Positions:** x, y is the CENTER of a shape or text, the top-left of a comment, the
midpoint of a line. Omit them to place at the board's center. Later elements draw on top.
### Create
- `{"op": "add", "role": "text" | "box" | "button" | "input" | "card" | "ellipse" | "line" | "image" | "comment" | "draw" | "triangle" | "diamond" | "star" | "frame", "x", "y", "w"?, "h"?, "id"?, ...style}`
Defaults (w x h): text 160 wide, box 180x110, card 240x150 (radius 12), button 150x46,
input 220x44, ellipse 140x90, line 180 long, triangle 120x110, diamond/star 120x120,
image 320x200 (needs "src": URL or data: URL), frame 360x240, draw needs "path".
Pass your own "id" to address the element without reading back (it must be unused).
Buttons and inputs are plain shapes: give them text with `label`.
- `{"op": "draw", "path": "M 10 10 L 90 90", "stroke"?, "strokeWidth"?}` freehand; path
coordinates are board coordinates.
- `{"op": "comment", "text", "x"?, "y"?, "w"?}` a sticky note for the human.
- `{"op": "label", "id": shapeId, "text", "fontSize"? = 16, "fill"?, "labelId"?}` text
centered inside a shape that follows it when the shape moves. Empty text removes it.
- `{"op": "duplicate", "id", "newId"?, "dx"? = 20, "dy"? = 20}`
### Change
- `{"op": "update", "id", ...style}` (fields below; "x"/"y"/"w"/"h" set position and size)
- `{"op": "move", "id", "dx", "dy"}` or `{"op": "move", "id", "x", "y"}`; moving a
frame moves its children too.
- `{"op": "reorder", "id", "to": "front" | "back" | "forward" | "backward"}`
- `{"op": "delete", "id"}` · `{"op": "clear"}` removes everything.
### Style fields (add and update)
fill, stroke, backgroundColor (CSS colors) · strokeWidth · opacity 0..1 · angle (degrees) ·
radius (corner radius; or rx, ry) · borderStyle "solid" | "dashed" | "dotted" | "dashdot" or
strokeDashArray [numbers] · shadow "none" | "soft" | "medium" | "hard", {color, blur, offsetX, offsetY}
or null · gradient {"type": "linear" | "radial", "stops": [{"offset": 0, "color": "#6366f1"},
{"offset": 1, "color": "#ec4899"}], "coords"?: 0..1 units} (replaces fill) · flipX, flipY,
skewX, skewY · text, fontSize, fontFamily, fontWeight, fontStyle ("italic"), textAlign,
underline, linethrough, lineHeight, charSpacing, padding · name (layer name), visible,
locked (true/false).
### Structure
- `{"op": "group", "ids": [2 or more], "name"?, "groupId"?}` returns the group id.
`{"op": "ungroup", "group": groupId}` or `{"op": "ungroup", "ids": [...]}`.
- `{"op": "wrap", "ids": [...], "direction"?, "justify"?, "align"?, "gap"?, "padding"?,
"frameId"?}` puts the elements in a new auto-layout frame and returns its id.
`{"op": "unwrap", "id": frameId}` removes the frame and keeps its children.
- `{"op": "reparent", "ids": [...], "frame": frameId}` moves elements into a frame;
`"frame": null` takes them out.
- `{"op": "layout", "id": frameId, "mode"?: "block" | "flex", "direction"?: "row" | "col",
"justify"?: "start" | "center" | "end" | "between", "align"?: "start" | "center" | "end" | "stretch", "gap"? = 16,
"padding"? = 16, "wMode"?, "hMode"?: "hug" | "fixed", "w"?, "h"?}` A flex frame lines
its children up (row = left to right, col = top to bottom) and hugs them, or keeps the
fixed w/h. A block frame leaves children where they are.
- `{"op": "size", "id", "w"?, "h"?: "fixed" | "hug" | "fill", "minW"?, "maxW"?, "minH"?,
"maxH"?}` how a child of a flex frame sizes; fill shares the frame's spare room; null
clears a min/max.
### Board
- `{"op": "bg", "color": "#f8fafc"}` or `{"op": "bg", "src": imageUrl}` (an image
behind everything).
- `{"op": "board", "w"?, "h"?, "autoH"?: true, "padding"?, "minH"?, "maxH"?}` autoH
grows the board to fit its content plus padding, within minH..maxH.
Errors name the failing op (`ops[2] (move): ...`): 400 for a bad field, 404 for an unknown
id, 409 for an id already in use. Fix that op and send the batch again.