cameraThe camera, as a value.
A 3-number state — x, y, z — with helpers for pan, zoom-to-fit, zoom-to-cursor, animated transitions, and frame-perfect screen↔world conversion.
canvas-harness is a node-graph library for React that renders on a canvas instead of the DOM — so 10k nodes pan at ~80fps. It owns the hard parts — camera, hit-testing, history, spatial queries — and ships no UI and no styles. Every color, font, and corner radius is yours.
store.addNode(node)
store.setCamera({ z: 1.4 })canvas-harness, 10,000 plain rects (top) vs Excalidraw, 8,000 (bottom) — live pan and zoom on a MacBook M1. Excalidraw is canvas-rendered and excellent; this is just where bitmap caching and viewport culling pull ahead at scale. Numbers vary on other hardware.
Screen, world, and node-local coordinates, converted correctly at any zoom or pan.
A uniform-grid spatial index backs querySpatial — point, rect, and marquee hits without walking the node list.
Visibility culling paints only the nodes in view — 10k visible nodes pan at ~80fps on an M1.
Pan, zoom, marquee-select, and drag-to-move come wired, switchable through the canvas tool prop.
Undo and redo over a typed operation log — the same log that drives collaboration through a SyncAdapter.
It paints every node to a canvas — bitmap-cached, with motion-based LOD — and ships zero styles, so the look is entirely yours.
cameraA 3-number state — x, y, z — with helpers for pan, zoom-to-fit, zoom-to-cursor, animated transitions, and frame-perfect screen↔world conversion.
hitA uniform-grid spatial index. Point, rect, and marquee queries run through querySpatial without walking the node list.
virtualVisibility culling paints only the nodes in view — 10k visible nodes pan at ~80fps. The same index powers the Minimap.
selectMulti-select, shift-add, marquee, group-bounds, transform handles, keyboard nudging, and clipboard — all driven from the store and surfaced through React hooks.
historyUndo and redo over a typed operation log, with coalescing. The same log syncs to collaborators through a SyncAdapter.
stylelessNodes paint to a canvas with bitmap caching and LOD. Styling is theme tokens you define — there's no bundled UI to fight.
exportSelection() / exportViewport() return a PNG of the board for a vision model.getContext(store, { format: "markdown" }) serializes the scene straight into a prompt.opSchemasAsAnthropicTools() hands the agent the op log as tool definitions; it mutates the board through tool calls.import {
exportSelection,
getContext,
opSchemasAsAnthropicTools,
} from "@canvas-harness/core";
const png = await exportSelection(store); // PNG for a vision model
const context = getContext(store, { format: "markdown" }); // scene → prompt
const tools = opSchemasAsAnthropicTools(); // the agent's write APIIt's how dim0.net's board-aware agent reads your board before it acts.
Op. The change event carries an OpBatch with previous-value slices — shaped for OT, CRDT, or any custom sync strategy.attachSync(store, adapter) wires any transport behind a SyncAdapter. Ships none — bring Yjs, WebSocket, or BroadcastChannel.store.presence, useLocalPresence() / usePresence() for live cursors and selections.import { attachSync } from "@canvas-harness/core";
import { createBroadcastSyncAdapter } from "@canvas-harness/sync-broadcast";
// every mutation is a typed op — wire any transport behind a SyncAdapter
const detach = attachSync(
store,
createBroadcastSyncAdapter({ channelName: "board-42", clientId: store.clientId }),
);A ready BroadcastChannel adapter ships as @canvas-harness/sync-broadcast — multiplayer across tabs in three lines.
pnpm add @canvas-harness/core @canvas-harness/react
# or npm i @canvas-harness/core @canvas-harness/reactimport { createCanvasStore } from "@canvas-harness/core";
const store = createCanvasStore();
store.addNode({ id: "a", x: 0, y: 0, w: 180, h: 100, type: "note" });
store.addNode({ id: "b", x: 240, y: 60, w: 200, h: 120, type: "note" });import { CanvasProvider, Canvas } from "@canvas-harness/react";
export function Board() {
return (
<CanvasProvider store={store}>
<Canvas tool="select" />
</CanvasProvider>
);
}import { useSelection, useCamera, useCanUndo } from "@canvas-harness/react";
const selection = useSelection(); // selected node ids
const camera = useCamera(); // { x, y, z } z = zoom factor
const canUndo = useCanUndo(); // store.undo() / store.redo() to stepcreateCanvasStore(opts?)CanvasStorecreate the store holding nodes, edges, camera, and selection<CanvasProvider store={...}>JSXput a store in context for the canvas and hooks below it<Canvas tool="select" />JSXthe canvas surface — paints nodes, handles pan / zoom / toolsuseSelection()string[]the selected node ids, reactiveuseCamera()Camerathe live camera — position and zoomuseCanUndo() / useCanRedo()booleanwhether undo / redo is currently availablestore.undo() / store.redo()voidstep history backward or forwardstore.querySpatial(rect)Node[]spatial query against the uniform-grid indexFull reference, types, and an interactive playground live on the repo readme.