The editor
The one object you talk to. How to create it, read its state, change the document in undoable steps, and move the camera.
The editor holds the document, the instance state (camera, selection, tool, styles) and every operation you can perform. Whatever surface you render on, you end up with the same Editor.
Getting an editor#
You rarely construct one by hand. Each host creates it for you:
| Where | How you get it |
|---|---|
| Plain JS | createCanvasFlow(el).editor |
| React | onMount={(editor) => …} on <CanvasFlow/>, or useEditor() inside it |
| React Native | onReady={(editor) => …} on <CanvasFlowView/> |
| Node, tests, workers | createEditor() from canvasflow |
createEditor() gives you an editor with every built-in shape, tool and action. Pass options to add your own or start from a snapshot:
import { createEditor } from 'canvasflow'
const editor = createEditor({
readonly: false,
historyLimit: 200,
initialState: { grid: 'dots', theme: 'dark' },
})
The full list is on the options page.
Two kinds of state#
The document is what you save and share: pages, shapes, bindings, assets. It lives in the store as immutable records.
The instance state is how this one person is looking at it: the camera, the current page, the selection, the active tool, the styles the next shape will use, the theme. It lives in editor.state, and it is never part of the document.
editor.state.selectedIds // ['shape:abc']
editor.state.toolId // 'select'
editor.state.theme // 'dark'
editor.camera // { x: 0, y: 0, z: 1 }
editor.updateState({ grid: 'lines', objectSnap: false })
Changing the document#
Create, update and delete shapes with plain objects. Ids are generated for you:
const id = editor.createShape({
type: 'geo',
x: 0,
y: 0,
props: { geo: 'ellipse', w: 160, h: 100, color: 'violet', fill: 'semi' },
})
editor.updateShape({ id, x: 40, props: { text: 'Hello' } })
editor.deleteShapes([id])
You only pass the props you want to change. Anything you leave out comes from the shape's defaults and the current styles, the same way a shape drawn with the toolbar would.
One action, one undo step#
Each call above is its own undo step. Wrap several changes in batch to make them one:
editor.batch(
() => {
const a = editor.createShape({ type: 'geo', x: 0, y: 0 })
const b = editor.createShape({ type: 'geo', x: 300, y: 0 })
editor.alignShapes([a, b], 'top')
},
{ label: 'Insert template' },
)
Use { history: 'ignore' } for changes the user shouldn't undo, like loading a starting scene. editor.undo() and editor.redo() work exactly like ⌘Z and ⇧⌘Z, and editor.canUndo tells you whether there's anything to undo.
Selection#
editor.select(id) // replace the selection
editor.addToSelection([other])
editor.selectAll()
editor.selectNone()
editor.getSelectedIds()
editor.getSelectedShapes()
editor.getOnlySelectedShape() // the shape when exactly one is selected
Selection is per page. Switching pages remembers what was selected on each.
Structure#
The editor has a method for every command in the default UI. A few you'll use often:
editor.alignShapes(ids, 'left')
editor.distributeShapes(ids, 'horizontal')
editor.groupShapes(ids)
editor.bringToFront(ids)
editor.duplicateShapes(ids)
editor.flipShapes(ids, 'horizontal')
editor.toggleLock(ids)
The Editor API lists them all.
Camera and coordinates#
There are two coordinate spaces. Screen is pixels in the canvas element. Page is the infinite drawing surface. Shapes live in page space.
editor.setCamera({ x: 0, y: 0, z: 1.5 }, { animate: true })
editor.zoomToFit({ animate: true })
editor.zoomToSelection()
editor.centerOnPoint({ x: 400, y: 300 })
const page = editor.screenToPage({ x: 120, y: 80 })
const screen = editor.pageToScreen(page)
Zoom is clamped between 5% and 1600%, and the zoom buttons step through the same stops as the keyboard (MIN_ZOOM, MAX_ZOOM and ZOOM_STEPS are exported if you build your own).
Finding shapes#
Hit-testing uses each shape's real geometry, with a tolerance that grows for touch and pen:
editor.getShapeAtPoint({ x: 100, y: 100 }) // the topmost shape under a page point
editor.getShapesInBox({ x: 0, y: 0, w: 500, h: 300 }, 'contain')
editor.getShapesInLasso(points)
editor.getShapePageBounds(id)
Pages#
const page = editor.createPage('Ideas')
editor.setCurrentPage(page)
editor.renamePage(page, 'Ideas, round 2')
editor.duplicatePage(page)
editor.getPages()
Each page keeps its own camera and selection.
Listening#
Everything that happens is an event. Two you'll use constantly:
const off = editor.on('change', ({ diff, source }) => {
// every document change: 'user', 'remote' or 'system'
})
editor.on('select', ({ ids }) => console.log(ids))
off() // every `on` returns its unsubscribe
The Events page has the full list. If you only need to know that something changed (to re-render your own UI), editor.subscribe(fn) is cheaper.
Read-only and themes#
editor.setReadonly(true) // viewing, panning, zooming and selecting still work
editor.setTheme('dark')
Cleaning up#
Hosts dispose their editor when they unmount. If you created one with createEditor(), call editor.dispose() when you're done with it.