0011. A free canvas element is a chromeless frame
Context: graphic elements (Text, Box, Ellipse, Path, Image) lived only inside frames, as clips of
the frame’s doc (motion_revisions). The product now wants them anywhere on the canvas: drawn
outside any frame, dragged out of a frame and back in, grouped with ⌘G. Next to them sit the AI
generation nodes, renamed “Generate text / image / video / audio / 3D model” so the two families
read apart; their type ids (text, image, …) do not change.
Decision: a free element is a frame node (type: 'motion') with data.kind: 'element'. Its doc
is a transparent still sized to the drawn box, holding the element as its clip; the node is the
box itself. kind decides the chrome, in one table (frameChrome, motion-node.ts): an element
has no play bar, no Open editor, no name tag, no ports, and posters as PNG so it stays
see-through. A group of free elements is the same thing with several clips.
The first draft of this ADR chose a new node type element holding one clip in nodes.data. It
was dropped before any code because the chromeless frame already has everything that draft would
have rebuilt:
- One renderer, one inspector, for real. The element is drawn by the frame compose path and edited by the frame editor and its Properties. Nothing renders or edits a clip outside a doc.
- Drag out already lands this way.
moveElementdropping on the canvas creates a frame holding the clip at its size (frameOf); a free element is that frame without chrome. Drag in ismoveElementfrom the element’s frame into the target, and the emptied element goes. - Realtime and concurrency. Each element is its own row and its own revision line:
versionguards each doc, position is last-write-wins, two people on two elements never conflict. - Performance. An element is a node with a poster, so the LOD poster layer and the #597 shells apply as they are. Only an element being edited mounts a live player.
- MCP parity.
draw_elementis one more canvas op over the samedrawElementthe page action calls; moves, deletes and undo are the node and frame ops that exist. - No migration.
motionis already innodes_type_check;kindis a field ofdata.
Discarded:
- A node type
elementwith the clip indata. It needs a renderer for a clip outside a doc, an inspector bound to node data instead of the doc history, its own undo, and a migration onnodes_type_check— a second copy of what frames already do. - An invisible canvas-level “board” doc. One
versionfor every element of the canvas, so any two editors conflict; the whole board renders as one unit, which defeats per-node LOD. - Reusing the generation nodes (
text,image). They carry prompts, models and runs; a drawn Text is not a generation.
Costs: every element carries a doc and a revision history — heavier than a row of data, cheap
next to a second renderer. A poster is a raster: zoomed far in, an element is soft until it is
opened. Groups: see below.
Groups (amended 2026-10-15)
Section titled “Groups (amended 2026-10-15)”A canvas group is a frame of kind group. It holds two things:
- Free elements, drawn inside its doc as clips, so they render, scale and edit with the group.
- Any other node (frames, generation nodes) as a member:
data.groupIdon the member names the group. A member stays its own node, so a frame in a group still opens, plays and edits. The group sits behind its members (z = -1), so they keep taking clicks.
Every node may carry data.rotation (degrees, -180..180), applied as the CSS rotate property
on the node wrapper; rotation and groupId are layout fields shared by all node types
(nodeLayoutSchema, node-layout.ts), validated alongside each type’s own schema. No column,
no migration.
Rotating a group turns it and moves every member around the group centre, adding the angle to
each (rotateGroup); moving a group moves its members by the same delta (translateGroup).
Discarded: SvelteFlow parent nodes (parentId) — positions become relative to the parent, so
every reader of nodes.x/y (MCP, agent, posters, LOD) would need to resolve the parent chain.
Not done: scaling a group with members (only its own clips scale), rotation undo.