Reference
Canvas State and Shape Objects
Look up syntax, contracts, layouts, algorithms, and exact behavior.
A canvas stores a JSON state object with an optional background color and ordered shape list. Shape ordering is the z-order: later shapes draw above earlier shapes. Every durable shape mutation is expressed as create, update, delete, reorder, update_canvas, or batch. The backend validates the operation surface before any mutation is written or broadcast.
- Rectangles and ellipses store x/y/width/height, stroke, fill, opacity, stroke width, rotation, and grouping metadata.
- Lines store endpoint coordinates, stroke styling, opacity, stroke width, and grouping metadata; rotation is represented by endpoint updates rather than a separate angle.
- Text shapes store a rectangular text box, text content, text color, text opacity, font size, alignment, wrapping behavior, and ordinary shape styling.
- The canvas itself can store backgroundColor, which is updated by the paint bucket when the user clicks the background.
- The ordered shapes array doubles as z-order, so bring-forward/send-back operations are durable reorder_shape operations.
The shape model is intentionally plain. Instead of building a deep object hierarchy with separate tables for rectangles, ellipses, text, groups, and lines, the canvas state stores serializable shape records in one JSON document. The editor can evolve quickly and realtime messages stay small: most edits are just patches against a shape id. The tradeoff is that the backend must be strict about validating which fields are allowed for which shape type.
This is a document-model choice before it is a database-model choice. The canvas is edited as one ordered JSON document because the operations users perform are document operations: draw this shape, patch that shape, move this group, reorder this layer. Relational tables still own users, access, folders, and history, but the drawing itself remains a compact serializable object.
type BaseShape = {
id: string;
type: "rect" | "ellipse" | "line" | "text";
groupId?: string | null;
groupIds?: string[] | null;
strokeColor: string;
fillColor: string;
strokeOpacity: number;
fillOpacity: number;
strokeWidth: number;
rotation?: number;
createdBy: string;
updatedAt: number;
};
type RectLike = BaseShape & { x: number; y: number; width: number; height: number };
type LineShape = BaseShape & { x1: number; y1: number; x2: number; y2: number };
type TextShape = RectLike & {
text: string;
textColor: string;
textOpacity: number;
fontSize: number; // validated as 4..512
textAlign?: "left" | "center" | "right";
};Text is intentionally whole-object styling rather than per-span rich text. A text shape can edit content, text color, text opacity, font size, and left/center/right alignment. Older text shapes without textAlign render as left-aligned. The toolbar keeps text controls visible for discoverability, but disables and mutes them unless the selection contains only unlocked text shapes.
The text editing flow is designed around commit boundaries. Double-clicking or creating a text shape opens an inline textarea inside the SVG foreignObject. While the user types, text is local editor state. Blur, Escape, Tab, or Cmd/Ctrl+Enter commits one update_shape operation. Typing stays responsive, character-by-character collaboration stays out of scope, and undo/redo receives a clear unit: the completed text edit.
That is a product and architecture choice, not a missing parser detail. Per-span rich text would require text-range operations, selection ranges, conflict handling inside a string, and more complex undo semantics. LiveBoard's current text object behaves like a styled diagram label: the whole label can be aligned, resized, colored, moved, grouped, rotated, and undone as one canvas object.
Grouping is represented as a stack rather than as a separate group node. Each shape may carry groupIds, where the last element is the current active group. Creating a parent group appends a new id to every selected unit. Ungrouping removes only the active id, preserving child groups below it.
select units -> unit can be one unlocked shape or one already-grouped object -> Group appends new parent group id to every selected unit -> selection reconciles to the new top group -> move/scale/rotate operate on the group as one unit Ungroup -> remove only the active/top group id -> preserve child group ids underneath
The stack representation keeps grouping compatible with the rest of the operation model. Moving a group is still a batch of shape patches, deleting a group is still a batch of shape deletes, and undoing either action still uses the same inverse machinery as ordinary shape edits. There is no separate group object whose lifetime could drift away from its members.