Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.deep.space/llms.txt

Use this file to discover all available pages before exploring further.

The useCanvas hook gives you a complete collaborative canvas surface: add and move shapes, broadcast viewports across peers, undo and redo, all synchronized over a CanvasRoom Durable Object. Use it for whiteboards, mood boards, diagram tools, and any “shapes on an infinite plane” UI.

Connect to a canvas

import { useCanvas } from 'deepspace'

function Whiteboard({ canvasId }: { canvasId: string }) {
  const {
    shapes,
    viewports,
    connected,
    addShape,
    moveShape,
    resizeShape,
    updateShape,
    deleteShape,
    setViewport,
    undo,
    redo,
  } = useCanvas(canvasId)

  // shapes is an array of CanvasShapeClient.
  // viewports is an array of ViewportClient - one entry per remote peer.
}
canvasId is any string. Two clients passing the same canvasId connect to the same CanvasRoom.

Shape lifecycle

A shape is an object with a position, size, and an arbitrary props payload. Each shape has:
type CanvasShapeClient = {
  id: string
  type: string
  x: number
  y: number
  width: number
  height: number
  rotation?: number
  props: Record<string, unknown>
  createdBy: string
  createdAt: string
  updatedAt: string
}
Add one with:
function onAddRect(x: number, y: number) {
  // addShape is synchronous and returns void. The server assigns the shape id
  // and broadcasts it back; let the next `shapes` render pick it up.
  addShape({
    type: 'rect',
    x, y,
    width: 100,
    height: 60,
    props: { color: 'blue', label: 'New' },
  })
}
The type is a free-form string - define your own vocabulary (rect, circle, arrow, text, image). The props field holds whatever metadata you need. Mutation methods are all positional and return void:
moveShape(shapeId, 200, 150)                  // (shapeId, x, y)
resizeShape(shapeId, 200, 120)                // (shapeId, width, height, x?, y?)
resizeShape(shapeId, 200, 120, 50, 80)        // also reposition while resizing
updateShape(shapeId, { color: 'red' })        // (shapeId, props) - props is merged
deleteShape(shapeId)

Rendering shapes

shapes is a flat array of CanvasShapeClient records. Render them however you like - SVG, canvas, DOM:
<svg width={800} height={600}>
  {shapes.map((s) => {
    if (s.type === 'rect') {
      return (
        <rect
          key={s.id}
          x={s.x}
          y={s.y}
          width={s.width}
          height={s.height}
          fill={(s.props.color as string) ?? 'gray'}
        />
      )
    }
    if (s.type === 'circle') {
      return (
        <circle key={s.id} cx={s.x} cy={s.y} r={s.width / 2} />
      )
    }
    return null
  })}
</svg>
When any peer adds, moves, or deletes a shape, the shapes array re-renders. Note that addShape and deleteShape round-trip through the server, so the calling peer also gets the update from the broadcast. moveShape, resizeShape, and updateShape broadcast only to other peers - if you want the local peer to see the change immediately, update your UI optimistically.

Viewports

Each connected peer broadcasts a viewport (position, size, and zoom). Use it to show “where everyone is looking” in minimap-style UIs:
const { viewports, setViewport } = useCanvas(canvasId)

function onPan(x: number, y: number, zoom: number, width: number, height: number) {
  // setViewport requires width and height (the visible viewport rect) in addition to x/y/zoom.
  setViewport({ x, y, zoom, width, height })
}

// viewports is a ViewportClient[] - one entry per remote peer (your own
// viewport is not included). Each entry includes `userId`.
for (const vp of viewports) {
  console.log(`${vp.userId} is at (${vp.x}, ${vp.y}) zoomed ${vp.zoom}`)
}
Viewports are ephemeral - they’re not persisted. When a peer disconnects, their viewport disappears.

Undo and redo

Each peer has an independent undo stack:
const { undo, redo } = useCanvas(canvasId)

<button onClick={undo}>Undo</button>
<button onClick={redo}>Redo</button>
Undo reverses the calling peer’s most recent edits only. It doesn’t touch other users’ actions.

Worker setup

The scaffold already wires AppCanvasRoom:
// worker.ts
export class AppCanvasRoom extends CanvasRoom<Env> {}

const app = new Hono<{ Bindings: Env }>()
app.get(
  '/ws/canvas/:docId',
  wsRoute((env) => env.CANVAS_ROOMS, () => ({ role: 'member' })),
)
You don’t need to edit either side unless you’re customizing shape persistence or adding server-side validation.

A complete whiteboard

import { useState } from 'react'
import { useCanvas } from 'deepspace'

export default function Whiteboard({ canvasId }: { canvasId: string }) {
  const { shapes, addShape, moveShape, deleteShape } = useCanvas(canvasId)
  const [selected, setSelected] = useState<string | null>(null)

  function onCanvasClick(e: React.MouseEvent<SVGSVGElement>) {
    const rect = e.currentTarget.getBoundingClientRect()
    addShape({
      type: 'rect',
      x: e.clientX - rect.left,
      y: e.clientY - rect.top,
      width: 80,
      height: 50,
      props: { color: '#1D4ED8' },
    })
  }

  return (
    <svg width="100%" height="100vh" onClick={onCanvasClick}>
      {shapes.map((s) => (
        <g key={s.id} onClick={(e) => { e.stopPropagation(); setSelected(s.id) }}>
          <rect
            x={s.x}
            y={s.y}
            width={s.width}
            height={s.height}
            fill={s.props.color as string}
            stroke={selected === s.id ? '#000' : 'none'}
          />
        </g>
      ))}
    </svg>
  )
}
Open the page in two tabs. Click in each tab to add shapes; both surfaces stay in sync.

When to use canvas vs Yjs

Both can model “shapes that multiple users edit.” Pick based on the merging needs:
Use canvas when…Use Yjs when…
Shapes are independent unitsShapes form a unified document
Last-write-wins per shape is fineYou need CRDT merging of overlapping edits
You need a built-in undo stackYou need fine-grained operation-log merging
Viewports / cursors are part of the surfaceYou want to use a third-party editor binding
Canvas is the right pick for whiteboards, mood boards, and most diagram tools. Yjs is the right pick when you’re integrating Tiptap, ProseMirror, or a similar collaborative document editor.

Next steps