a canvas engine for React

Canvas performance, zero style opinions.

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.

MIT canvas-rendered headless + styleless ts-first
open playground →
board
notes · code · agents, one canvas
headless
styleless
store.addNode(node)
store.setCamera({ z: 1.4 })
canvas-rendered
100%
drag to pan · scroll to zoom · grab a card
live · the same engine behind dim0.net, painted on a canvas, not the DOM
see for yourself

10k nodes, panning live.

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.

why a harness

Every canvas-shaped product re-solves the same six problems. Stop re-solving them.

01

Coordinate spaces

Screen, world, and node-local coordinates, converted correctly at any zoom or pan.

02

Hit testing

A uniform-grid spatial index backs querySpatial, point, rect, and marquee hits without walking the node list.

03

Render virtualization

Visibility culling paints only the nodes in view, 10k visible nodes pan at ~80fps on an M1.

04

Gesture choreography

Pan, zoom, marquee-select, and drag-to-move come wired, switchable through the canvas tool prop.

05

History

Undo and redo over a typed operation log, the same log that drives collaboration through a SyncAdapter.

06

Canvas-rendered, styleless

It paints every node to a canvas, bitmap-cached, with motion-based LOD, and ships zero styles, so the look is entirely yours.

what's in the box

Six primitives. Compose to taste.

viewport
01camera

The 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.

hit
02hit

Hit-test anything.

A uniform-grid spatial index. Point, rect, and marquee queries run through querySpatial without walking the node list.

03virtual

Render only what's visible.

Visibility culling paints only the nodes in view, 10k visible nodes pan at ~80fps. The same index powers the Minimap.

04select

Selection, the boring parts done.

Multi-select, shift-add, marquee, group-bounds, transform handles, keyboard nudging, and clipboard, all driven from the store and surfaced through React hooks.

undo · redo · branch
05history

History that doesn't bite.

Undo and redo over a typed operation log, with coalescing. The same log syncs to collaborators through a SyncAdapter.

DOMcanvas2d
06styleless

Bring your own look.

Nodes paint to a canvas with bitmap caching and LOD. Styling is theme tokens you define. There's no bundled UI to fight.

beyond rendering

Made for agents and multiplayer.

ai-ready

A canvas your agent can see, read, and write.

  • See: exportSelection() / exportViewport() return a PNG of the board for a vision model.
  • Read: getContext(store, { format: "markdown" }) serializes the scene straight into a prompt.
  • Write: opSchemasAsAnthropicTools() hands the agent the op log as tool definitions; it mutates the board through tool calls.
ts
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 API

It's how dim0.net's board-aware agent reads your board before it acts.

collab-ready

Multiplayer-shaped from the core.

  • Every mutation is a typed 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.
  • Presence is built in: store.presence, useLocalPresence() / usePresence() for live cursors and selections.
ts
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.

quick start

Hello, infinite canvas.

01 · INSTALL
bash
pnpm add @canvas-harness/core @canvas-harness/react
# or  npm i @canvas-harness/core @canvas-harness/react
02 · CREATE A STORE
ts
import { 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" });
03 · RENDER
ts
import { CanvasProvider, Canvas } from "@canvas-harness/react";

export function Board() {
  return (
    <CanvasProvider store={store}>
      <Canvas tool="select" />
    </CanvasProvider>
  );
}
04 · REACT TO IT
ts
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 step
api at a glance

The whole surface fits on one page.

createCanvasStore(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 / tools
useSelection()string[]the selected node ids, reactive
useCamera()Camerathe live camera, position and zoom
useCanUndo() / useCanRedo()booleanwhether undo / redo is currently available
store.undo() / store.redo()voidstep history backward or forward
store.querySpatial(rect)Node[]spatial query against the uniform-grid index

Full reference, types, and an interactive playground live on the repo readme.

built on it

Built to power a real product.

frequently asked

Common questions.

Yes. The state core (@canvas-harness/core) is framework-neutral, but rendering and the hooks live in @canvas-harness/react, which lists React ≥18 as a peer dependency. React is the supported target today.