A high-performance infinite canvas SDK for large, media-heavy, AI-native boards
Flat document model · PixiJS rendering · custom nodes · one contract for Capabilities / Plugins / Agents
🎮 Live interactive demo · 开发者文档 · 架构与交付文档 · ADRs · 简体中文
PixiBoardJS is an infinite canvas SDK extracted from a real desktop media application: the document is a flat, serializable JSON tree, and the PixiJS scene is nothing more than a disposable viewport render cache. It is not another general-purpose scene-graph graphics library — it's purpose-built for large, sparse, media-dense canvases where images, video, audio, markdown, model previews, and AI-generated content all live in the same document model.
This repository is transitioning from architecture planning to a shippable SDK. The core packages (
core/renderer-pixi/pixiboardjs) are implemented and tested — see the delivery roadmap for stage-by-stage acceptance status.
👉 Try the live demo — this runs the real pixiboardjs package, not a recording. Drag nodes, pan/zoom, undo/redo, and toggle custom node state directly.
Two things here are structural, not incremental — they follow from the data model rather than from tuning.
Live render objects track what's on screen, not how big the document is. The document is flat JSON; the PixiJS scene is a viewport cache the renderer builds and discards. A spatial index answers "what is visible," and only those nodes get render objects. In the benchmark below, growing the document from 10k to 100k nodes leaves the number of live render objects flat.
Agent writes go through the same transaction pipeline as human edits. There is no separate automation API. An agent's canvas.write produces the same revision, the same ChangeSet, and the same history entry a drag would — so it is undoable, observable, and tagged with an origin. Most canvas libraries have no notion of a non-human writer at all; the ones that expose an editor API let automation bypass history and access control.
| PixiBoardJS | Konva / Fabric.js | tldraw | |
|---|---|---|---|
| Live render objects | Track visible content | All nodes retained | All nodes retained |
| Agent writes | Same transaction pipeline as human edits — undoable, audited, origin-tagged |
Not included | No first-class agent contract |
| Data model | Flat, serializable JSON document | Nested scene graph (parent/child) | Document model + built-in sync |
| Rendering | PixiJS / WebGL | Canvas2D | React / DOM |
| Undo/redo & history | Built-in, transaction-based | Not included | Built-in |
| Access control (UI/plugin/agent) | Built-in capabilities contract |
Not included | Not included |
| Custom node types | Unified registry, no SDK core changes needed | Custom subclasses | Custom shape classes |
| Platform targets | Browser + Tauri WebView via ports/adapters | Browser | Browser |
| License | MIT | MIT | Source-available; paid license for production SDK use |
| Real-time collaboration | Not in v1 | Not included | Built-in sync engine |
Pick PixiBoardJS for a whiteboard, moodboard, or AI generation canvas where the document is large and media-dense, and where agents or plugins write to it alongside people. Pick tldraw if you want the best-in-class React editing experience and need multiplayer today. Pick Konva/Fabric/raw PixiJS if you want a general 2D graphics toolkit and intend to own the document model yourself.
- Flat data model — the document is plain JSON; nodes have no parent/child nesting. Any render object can be destroyed and rebuilt from data at any time.
- Viewport virtualization, on by default —
createPixiBoard()culls to the visible world rectangle (plus a configurable margin) as soon as the host reports a surface size. Spatial indexing, level-of-detail tiers, and texture lifecycle management ship with it. - Node Type Registry — built-in rect/text/image/video/audio nodes and user-defined custom nodes share one registration mechanism; adding a node type never requires touching an SDK-internal union type.
- Single write channel — every mutation (user interaction, public API, plugin, agent) flows through the same transaction/command pipeline, so undo/redo, events, and persistence stay naturally consistent.
- Capabilities boundary — a unified, permissioned surface (
canvas.read/canvas.write, etc.) for UI, plugins, and agents that never depends on private implementation details. - Web + Tauri — the same core and capabilities run in modern browsers and in a Tauri WebView; platform differences are injected via ports/adapters.
- One user-facing package — everyday consumers only need
pnpm add pixiboardjs; internal package boundaries exist for dependency direction and test isolation, not to be exposed to users.
pnpm add pixiboardjsimport { createPixiBoard } from "pixiboardjs";
const board = await createPixiBoard({
container: document.querySelector("#board"),
});
await board.ready;
await board.nodes.create({
type: "rect",
x: 40,
y: 40,
width: 120,
height: 80,
props: { fill: 0x7c8cf8 },
});
board.viewport.fitBounds({ minX: 0, minY: 0, maxX: 400, maxY: 300 });
board.history.undo();A fuller, copy-pasteable external-consumer example lives in apps/examples-vanilla; a custom-node example is in apps/examples-custom-node; a Tauri desktop integration example is in apps/examples-desktop-sdk; and the source for the live demo at the top of this README is in apps/site.
An agent doesn't get a side door into the canvas. canvas.write lands in the same transaction pipeline a pointer drag does:
pnpm add @pixi-board/agent-toolsimport { createPixiBoardAgentTools } from "@pixi-board/agent-tools";
const tools = createPixiBoardAgentTools(board.capabilities);
await tools.call("canvas.write", {
type: "create",
nodes: [{ type: "rect", x: 40, y: 40, width: 120, height: 80 }],
});
board.history.undo(); // the agent's write undoes like any other editWhat that buys you:
- Undoable — the write produces a normal history entry. A user can undo an agent's work without a bespoke rollback path.
- Audited — every write carries an
origin(user/api/plugin:<id>/agent:<id>; agent tools default toagent:canvas), so you can tell who changed what. - Consistent — UI, plugin, and agent paths produce the same document, revision, and ChangeSet. This is asserted field-by-field in
packages/agent-tools/src/contract.test.ts, not just intended. - Headless-capable — document reads and writes work without a renderer. Tools that need a mounted canvas (preview, capture) report capability unavailability rather than assuming a renderer exists.
Read paths are agent-shaped too: canvas.read returns compact node DTOs with field projection and pagination, so a large board doesn't have to arrive as one enormous blob of JSON.
agent-tools is a contract, not a server. It hands you two tool definitions with JSON Schemas (import { canvasReadSchema, canvasWriteSchema } from "@pixi-board/agent-tools/schemas") and an async call(name, input). Wiring that to MCP, HTTP, a WebSocket, or a direct function call in your own agent loop is a few lines against whatever harness you already run — so the SDK doesn't ship a server and doesn't chase a moving protocol.
board.capabilities is public on its own. If you have your own tool schema, your own DTO shape, or your own agent framework's conventions, build directly against it:
import { createBoardCapabilities, isCapabilityError } from "@pixi-board/capabilities";
const result = await board.capabilities.nodes.create(
{ nodes: [{ type: "rect", x: 0, y: 0, width: 100, height: 100, rotation: 0, zIndex: 0 }] },
{ origin: "agent:my-agent" },
);Same transaction pipeline, same ChangeSet, same undo — you just own the translation layer. agent-tools is the convenience layer over this, not a privileged path around it.
┌─────────────────────────┐
│ pixiboardjs │ the only package users install
│ createPixiBoard / NodeHandle
└────────────┬─────────────┘
┌────────────┬─────────┼─────────┬────────────┐
│ │ │ │ │
┌──────▼─────┐┌─────▼──────┐┌─▼───────┐┌▼──────────┐┌▼───────────┐
│capabilities││renderer-pixi││ adapter- ││ adapter- ││ agent-tools │
│ (UI/plugin/││ (PixiJS ││ browser ││ tauri ││ (canvas.read│
│ agent ││ render ││(IndexedDB││ (WebView ││ /.write + │
│ contract) ││ cache) ││ /OPFS) ││ filesystem)││ JSON Schema)│
└──────┬─────┘└─────┬───────┘└─────────┘└────────────┘└─────────────┘
│ │ transport (MCP/HTTP/direct)
│ │ is yours to assemble
│ │
└─────┬──────┘
│
┌────▼─────┐
│ core │ document is the source of truth; no DOM/Pixi/Tauri awareness
│ document /│
│ store / │
│ history / │
│ viewport │
└──────────┘
- Data is truth, rendering is a cache — anything that must persist lives in
BoardDocument; Pixi containers, textures, and transient animation state are never the source of truth. - Core is platform-agnostic —
corenever imports DOM, Pixi, Tauri, or a plugin SDK; all platform capabilities are injected via ports/adapters. - See product goals & scope and target architecture for the full design rationale.
| Package | Description | Visibility |
|---|---|---|
pixiboardjs |
The single user-facing package: createPixiBoard(), NodeHandle, built-in nodes, capabilities facade |
Public |
@pixi-board/core |
Document / store / transaction / history / selection / viewport — no DOM dependency | Public |
@pixi-board/renderer-pixi |
PixiJS renderer: scene, spatial index, viewport virtualization, texture lifecycle | Internal |
@pixi-board/capabilities |
Unified permissioned read/write surface for UI / plugins / agents | Public |
@pixi-board/agent-tools |
canvas.read / canvas.write tool contracts and their JSON Schemas |
Public |
@pixi-board/adapter-browser |
IndexedDB / OPFS / ObjectURL persistence and asset adapters | Internal |
@pixi-board/adapter-tauri |
Tauri WebView filesystem adapter | Internal |
@pixi-board/plugin-sdk / plugin-api-v3 |
Plugin definePlugin() and the v3 capability contract |
Public / Internal |
Full dependency direction and ownership boundaries: package boundaries.
The claim isn't "faster than X." It's that the renderer's workload is decoupled from document size. Panning a fixed viewport across three documents that differ by 10× in node count:
| Document nodes | Live render objects |
|---|---|
| 10,000 | ~360 |
| 50,000 | ~360 |
| 100,000 | ~360 |
Frame work follows that curve rather than the node count. This is the property to design around; absolute milliseconds depend on your nodes, your assets, and your GPU.
How to read this number. It's an excerpt from one local canonical run on 2026-08-07 (Chromium 151, 1920×1080, DPR 1, seed 42, 30 warmup + 120 sampled frames), recorded as evidence-only: there is no fixed-machine baseline and no approved absolute budget, so no performance gate has passed. The run used ANGLE SwiftShader, which does not represent hardware GPU throughput, and the workload is sparse rectangular cards. It does not support a claim that PixiBoardJS is faster than Konva across canvas workloads — the benchmark doc records the cases where it isn't, including a 100k full-retained p95 of 41.80 ms.
Structural invariants the benchmarks hold to:
- Document node count ≠ Pixi DisplayObject count; ID lookups are O(1). Spatial index cost is per-node, independent of N (uniform grid: O(cells a node covers)).
- Pan/zoom hot paths scale with visible and preloaded node count; a single-node update never triggers a full scene rebuild.
- Level-of-detail tiers give node renderers a cheaper path when zoomed out far enough that culling can't help — every node is legitimately on screen.
- On-demand rendering when idle (no animation/video/interaction); views, texture leases, listeners, and tickers return to baseline after destroy.
pnpm benchmark:run # local headless benchmark (10k/50k/100k-node datasets, etc.)
pnpm benchmark:browser # real Chromium benchmark
pnpm benchmark:check # compare against historical baseline, detect regressionsSee performance goals & benchmarks for the full data, including the failing cases.
pnpm install
# static boundary and doc checks
pnpm docs:check
pnpm packages:check
# layered test suites
pnpm test:core # @pixi-board/core
pnpm test:contracts # capabilities / agent-tools contracts
pnpm test:adapters # adapter-browser / adapter-tauri / contract test suite
pnpm test:browser # Chromium renderer contract
# build publishable artifacts
pnpm build:releaseSee CONTRIBUTING.md for the full contribution workflow. CI runs static boundary checks, core contract tests, Chromium browser contracts, and a performance regression gate on every push/PR — see .github/workflows/ci.yml; desktop Tauri integration gates for macOS/Windows live in desktop-launch-smoke.yml.
- Docs index & recommended reading order
- Product goals & scope · Current-state assessment · Target architecture
- Package boundaries · Public API design · Custom node system
- Capabilities, plugins & agents · Platform, assets & persistence
- Code migration plan · Delivery roadmap & acceptance
- Performance goals & benchmarks · Testing, release & compatibility strategy
- Risks & open decisions · Goal traceability matrix
- Architecture Decision Records (ADR)