Infinite Canvas
Overview
Section titled “Overview”@xnetjs/canvas provides an infinite, zoomable 2D surface where users can place, connect, and arrange nodes. The canvas supports real-time collaboration via Yjs — multiple users can simultaneously add, move, and connect nodes.
Canvas component
Section titled “Canvas component”import { Canvas } from '@xnetjs/canvas'
;<Canvas doc={ydoc} // Required: Yjs document awareness={awareness} // Cursor presence renderNode={(node) => <MyCard />} // Custom node content onNodeDoubleClick={(id) => open(id)} onBackgroundClick={() => deselect()} config={{ minZoom: 0.1, maxZoom: 3, gridSize: 20, showGrid: true }}/>Keyboard shortcuts
Section titled “Keyboard shortcuts”| Shortcut | Action |
|---|---|
Delete / Backspace | Delete selected nodes |
Cmd+A | Select all |
Escape | Clear selection |
Cmd+1 | Fit to content |
Cmd+0 | Reset view |
Mouse and touch interaction
Section titled “Mouse and touch interaction”- Scroll — Pan the canvas
- Ctrl/Cmd + Scroll — Zoom
- Two-finger pinch — Zoom (trackpads and touch screens, including Safari gestures)
- Click node — Select it
- Click edge — Select it (edges have generous invisible hit areas)
- Shift/Cmd + Click — Additive selection
- Drag node — Move it
- Drag resize handles — Resize (8 handles on selection)
- Drag a connector handle — Draw a new edge to another node (handles appear on hover)
- Double-click — Open node, or edit its text in place
Editing on the canvas
Section titled “Editing on the canvas”Nodes and edges are directly editable without leaving the canvas:
- Edges — click to select; a floating toolbar offers a label input, an edge-kind picker, reverse direction, and delete.
- Drag-to-connect — drag from a node’s connector handle (shown on hover) and drop on another node to create an edge.
- Node text — double-click a canvas-native node (shape, note) to edit its
text inline. Source-backed cards (a page or database dropped onto the
canvas) keep their source’s title; press
F2or use the Rename action to give the card a local alias instead.
All edits go through Yjs transactions and sync live to collaborators.
useCanvas hook
Section titled “useCanvas hook”The primary hook that wires together the store, viewport, and layout engine:
const { // State nodes, edges, selectedNodeIds, viewport,
// Node operations addNode, updateNodePosition, removeNode,
// Edge operations addEdge, removeEdge,
// Selection selectNode, selectAll, clearSelection, deleteSelected,
// Viewport pan, zoomAt, fitToContent, resetView,
// Layout autoLayout, layoutSelected,
// Queries findNodeAt, findNodesInRect, getVisibleNodes} = useCanvas({ doc: ydoc, config })Spatial index
Section titled “Spatial index”The canvas uses an R-tree (rbush) for efficient spatial queries. This enables viewport culling — only nodes visible on screen need to be rendered.
// Find all nodes in the current viewportconst visibleIds = store.getVisibleNodes(viewport.getVisibleRect())
// Find the topmost node at a pointconst nodeId = store.findNodeAt(point)
// Find all nodes overlapping with a given nodeconst overlapping = store.findIntersecting(nodeId)The spatial index is automatically updated when nodes are added, moved, or removed.
Layout algorithms
Section titled “Layout algorithms”The LayoutEngine supports automatic layout via ELK.js and two built-in algorithms:
ELK.js layouts
Section titled “ELK.js layouts”await autoLayout({ algorithm: 'layered', // Sugiyama/hierarchical direction: 'RIGHT', // RIGHT, LEFT, DOWN, UP nodeSpacing: 50, layerSpacing: 100, edgeRouting: 'ORTHOGONAL' // POLYLINE, ORTHOGONAL, SPLINES})Available algorithms: layered, force, mrtree, radial, stress, box.
Grid layout
Section titled “Grid layout”layoutEngine.layoutGrid(nodes, { columns: 4, spacing: 20, padding: 50})Circle layout
Section titled “Circle layout”layoutEngine.layoutCircle(nodes, { radius: 300 // Auto-calculated if omitted})Nodes with properties.fixed = true keep their positions during layout.
Viewport
Section titled “Viewport”The Viewport class manages the camera:
pan(dx, dy)— Move by screen-space deltazoomAt(x, y, factor)— Zoom while keeping the point under cursor stationaryfitToRect(rect, padding)— Fit viewport to show a rectangle (never zooms beyond 100%)reset()— Return to origin at zoom 1screenToCanvas(x, y)— Convert screen coordinates to canvas spacecanvasToScreen(x, y)— Convert canvas coordinates to screen spacegetVisibleRect()— The canvas-space rectangle currently on screen
Data storage
Section titled “Data storage”Canvas state is stored in three Yjs maps inside the Y.Doc:
| Map | Contents |
|---|---|
metadata | Title, created/updated timestamps |
nodes | CanvasNode objects keyed by ID |
edges | CanvasEdge objects keyed by ID |
All mutations go through Yjs transactions, so changes sync automatically to other peers. The spatial index updates reactively when the Yjs maps change.
Node types
Section titled “Node types”interface CanvasNode { id: string type: string // 'page', 'database', 'note', etc. position: { x: number y: number width: number // Default: 200 height: number // Default: 100 rotation?: number zIndex?: number collapsed?: boolean } properties: Record<string, unknown> linkedNodeId?: string // Link to a page/database node}Edges connect nodes with optional labels, arrow markers, and routing styles (bezier or orthogonal).
Comments
Section titled “Comments”The canvas supports spatial comments via useCanvasComments:
- Position pins — Fixed canvas coordinates. Never orphaned.
- Object-attached pins — Follow a node’s position. Become orphaned when the node is deleted.
Further reading
Section titled “Further reading”- Collaboration Guide — Real-time editing patterns
- Sync Guide — How Yjs sync works