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

  • SeeexportSelection() / exportViewport() return a PNG of the board for a vision model.
  • ReadgetContext(store, { format: "markdown" }) serializes the scene straight into a prompt.
  • WriteopSchemasAsAnthropicTools() 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.