This page ships a Markdown twin for AI tools.

Guides

Canvas docs

Install @texuli/canvas, load a board, edit it, export SVG or PNG. In development.

Back to overview

What Canvas is

Canvas is an embeddable whiteboard: @texuli/canvas holds a framework-free model of shapes, text, connectors, groups and frames, typed operations with an op log, CRDT collaboration on @texuli/document/sync, a Svelte 5 editor and viewer, and SVG or PNG export with no HTML sinks. The package is published as 0.18.0 and is **in development** (pre-1.0). Pin the exact version or use ^0.17.0.

It is a board you embed. It does not host canvases, persist them, or serve images. The host app stores the document and resolves image refs.

What you get

  • A validated canvas as data (format: "texuli-canvas", formatVersion: 1).
  • Shapes, sticky notes, frames, groups, and connectors that route straight, elbow or curved.
  • CanvasEditor and CanvasViewer, with keyboard navigation and a shape tree.
  • Local collaboration through the document sync adapter, and canvasToSvg / canvasToPng.

There is no hosted realtime service. Persistence, transport and image hosting belong to the host.

Install

Svelte 5 (^5.57.2) is a peer. Import the token stylesheet once.

npm install @texuli/canvas
import '@texuli/tokens/base.css';

Load and edit

Canvases from a file, an agent or a share link go through loadUntrustedCanvas first. Keep the document in $state.raw.

<script lang="ts">
  import { CanvasEditor } from '@texuli/canvas/editor';
  import { loadUntrustedCanvas } from '@texuli/canvas/model';
  import '@texuli/tokens/base.css';

  let { json }: { json: string } = $props();
  const loaded = loadUntrustedCanvas(json);
  let canvas = $state.raw(loaded.ok ? loaded.canvas : null);
</script>

{#if canvas}
  <CanvasEditor bind:canvas resolveImage={(ref) => `https://cdn.example.com/${ref}.png`} />
{/if}

Build from operations

import { applyCanvasOps, createCanvas, moveOps } from '@texuli/canvas/model';

const canvas = createCanvas({ title: 'Launch plan' });
const added = applyCanvasOps(canvas, [
  { op: 'add', shape: { id: 'a', type: 'sticky', x: 0, y: 0, width: 160, height: 160, text: 'Ship' } },
]);

Format

The canonical document is data: format, formatVersion, optional meta and grid, then shapes and connectors. Array order is z-order. Coordinates are world units. Shape types include rect, ellipse, diamond, line, path, text, sticky, image, frame and group. Connectors route straight, elbow or curved. Styles are a closed token palette; raw CSS colors are refused.

import { applyCanvasOps, createCanvas, validateCanvas } from '@texuli/canvas/model';

validateCanvas and loadUntrustedCanvas refuse unknown keys, hostile text, image refs the URL policy rejects, and anything past CANVAS_LIMITS. Work is linear in the input. The model has no Svelte and no DOM.

Operations

Typed operations have inverses. The op log is the single edit path: undo, transforms, grouping and clipboard all go through it. The clipboard is untrusted JSON: size-checked, then validated.

Editor and viewer

CanvasEditor from @texuli/canvas/editor is the role="application" surface: toolbar, minimap, shape tree, pan, zoom, snapping and transforms. CanvasViewer from @texuli/canvas/viewer is read-only and exposes the structure as text.

Image refs are resolved by the host. Every URL passes safeImageSrc at the untrusted tier by default.

Export

import { canvasToPng, canvasToSvg } from '@texuli/canvas/export';

const svg = canvasToSvg(canvas, { resolveImage: (ref) => `https://cdn.example.com/${ref}.png` });
const png = await canvasToPng(canvas, { scale: 2 });

canvasToSvg writes escaped markup through @texuli/core/markup. canvasToPng is browser-only.

Sync

@texuli/canvas/sync binds the canvas to @texuli/document/sync. In-memory and BroadcastChannel adapters ship; a network transport is the host's. Collaboration is local or in memory, not a hosted service.

Keyboard and tree

The surface is the only role="application" region, named by the canvas title. Shapes take a roving tab stop in reading order. The shape tree (role="tree") lists every shape and connector for screen readers. Changes are announced through the shared live region.

Motion and contrast

Zoom and fit play through texuli/motion and are instant under prefers-reduced-motion. Forced colors keep outlines, focus and selection visible. Text paints as SVG text nodes; nothing is parsed as markup.

Status

Canvas is **in development**. @texuli/canvas is on npm as 0.18.0. Under 0.x a new minor may break. The Kotlin twin validates canvas nodes and does not paint them yet.

Not built

  • A hosted realtime collaboration service (adapters are local or in memory)
  • Persistence, network transport and image hosting (the host supplies them)
  • Native painting of canvases

Untrusted canvases

A canvas from an agent, a file or a share link is untrusted data. Use loadUntrustedCanvas or validateCanvas from @texuli/canvas/model before any deep work. Default URL policy is the untrusted tier. There is no HTML sink.

Catalog and player

Agents emit canvases as data through the canvas catalog id (blueprint spec 0.8.0). @texuli/catalog validateDraft refuses a canvas node unless the host supplies checkCanvas. @texuli/player paints a valid canvas root through its canvasHost seam.

import { createCanvasPlayerHost } from '@texuli/canvas/host';

const canvasHost = createCanvasPlayerHost({ resolveImage: (ref) => `/assets/${ref}.png` });

Do not invent an MCP operation that is not in @texuli/tools. Native painting of agent canvases is planned.