<bmx-infinite-canvas>
An infinite canvas for visual collaboration: sticky notes, shapes, text, images, frames and your own HTML on a plane without edges, joined by connectors that follow them as they move. Pan and zoom with a mouse, a trackpad, touch or the keyboard; select, drag, resize, turn, align, connect, type, undo - all in the page, with no server.
35 properties · 9 events · 44 methods · 26 parts
Example
Show markup
<div class="row" role="group" aria-label="Board">
<bmx-button id="ex-ic-note" variant="soft">Add a note</bmx-button>
<bmx-button id="ex-ic-fit" variant="soft">Show everything</bmx-button>
<bmx-button id="ex-ic-undo" variant="ghost">Undo</bmx-button>
</div>
<bmx-infinite-canvas id="ex-ic" label="Retrospective board" style="--bmx-infinite-canvas-height: 26rem; margin-block-start: 1rem"></bmx-infinite-canvas>
<div class="row" style="margin-block-start: 1rem">
<span class="note" id="ex-ic-out">
<strong>Drag, type, connect.</strong> Double-click the board for a note, drag from the dots round an item to
connect it, and press <kbd>Delete</kbd> to remove what is selected. Every change is reported here as the JSON
operations a multiplayer server would pass on to the other copies of the board.
</span>
</div>
<script type="module">
await customElements.whenDefined('bmx-infinite-canvas');
const board = document.getElementById('ex-ic');
const out = document.getElementById('ex-ic-out');
// The board is data in: nodes and edges, as plain objects.
await board.loadSnapshot({
nodes: [
{ id: 'went-well', type: 'frame', x: 0, y: 0, w: 460, h: 300, text: 'Went well', color: 'green' },
{ id: 'to-change', type: 'frame', x: 520, y: 0, w: 460, h: 300, text: 'To change', color: 'pink' },
{ id: 'n1', type: 'note', x: 30, y: 40, w: 180, h: 180, text: 'Shipped the share dialog on time', color: 'green' },
{ id: 'n2', type: 'note', x: 240, y: 40, w: 180, h: 180, text: 'Design partners loved the beta', color: 'yellow' },
{ id: 'n3', type: 'note', x: 550, y: 40, w: 180, h: 180, text: 'Too many late scope changes', color: 'pink' },
{ id: 'a1', type: 'shape', shape: 'rounded', x: 770, y: 80, w: 180, h: 100, text: 'Freeze scope a week earlier', color: 'blue' },
],
edges: [{ id: 'e1', from: 'n3', to: 'a1', label: 'action' }],
});
await board.zoomToFit(undefined, false);
// Changes out: send `ops` to the other copies, which apply them with `applyRemote()`.
board.addEventListener('bmxCanvasStateChange', event => {
out.textContent = `bmxCanvasStateChange (${event.detail.origin}) - ${JSON.stringify(event.detail.ops)}`;
});
board.addEventListener('bmxSelectionChange', event => {
if (event.detail.nodes.length) out.textContent = `bmxSelectionChange - ${event.detail.nodes.join(', ')}`;
});
document.getElementById('ex-ic-note').addEventListener('click', async () => {
const view = await board.getViewport();
const [note] = await board.addNodes({ type: 'note', color: 'violet', text: 'A new thought', x: view.x + view.width / 2 - 100, y: view.y + view.height / 2 - 100 });
await board.select(note.id);
});
document.getElementById('ex-ic-fit').addEventListener('click', () => board.zoomToFit());
document.getElementById('ex-ic-undo').addEventListener('click', () => board.undo());
// In a shared session, apply what arrives from the others:
// socket.onmessage = message => board.applyRemote(JSON.parse(message.data));
</script>
FAST
The board is indexed by position, and only what is in view is drawn: the items near the viewport as real, sharp, editable DOM, and - zoomed far out, or when thousands are in view - every item painted onto one canvas, so panning and zooming stay at the display's frame rate however large the board grows.
MULTIPLAYER
The drawing is kept apart from the data. Every change made here comes out
of bmxCanvasStateChange as JSON operations; hand them to applyRemote()
on every other copy - over a WebSocket, WebRTC, a CRDT provider, anything -
and all copies converge, whatever order the operations arrive in. Drags
stream out of bmxNodeMove as they happen, pointers out of bmxCursorMove,
and peers shows the other users' cursors and selections.
Or let the canvas do all of that: channel shares the board with the other
tabs of the browser with no server at all, and joinRoom() takes any relay
that forwards text between users - presence, following another user's view,
the laser pointer and bringing a late joiner up to date included.
WORKSHOPS
A pen, a highlighter and an eraser; votes counted per user; groups; align, space and tidy; find; templates for a retrospective, a kanban board, a SWOT analysis, a flowchart and a mind map; pictures dropped or pasted in.
YOURS
Give a node type: 'html' and a slot name, and any element of the page
with that slot appears inside it, moving and zooming with the board: a
chart, a video, a form, a component from any framework.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
changeBatch |
change-batch |
number |
0 |
Gather changes for this many milliseconds into one bmxCanvasStateChange. 0 sends each at once. |
channel |
channel |
string |
— | Share this board with every other canvas in the same browser that has the same channel - other tabs and windows of the site - with no server. For users on other machines, pass a relay to joinRoom(). |
clientId |
client-id |
string |
— | This copy's id in a shared session: part of every change it makes. Read once, when the element loads. |
cursorThrottle |
cursor-throttle |
number |
50 |
bmxCursorMove at most this often. |
edgeRoute |
edge-route |
BmxCanvasRoute |
'curved' |
How new connectors run. |
edges |
edges |
BmxCanvasEdge[] | string |
[] |
The connectors. Setting the list makes the board hold exactly these. |
fitOnLoad |
fit-on-load |
boolean |
true |
Frame the board when it loads. |
follow |
follow |
string |
— | Follow another user's view: their peer id. Panning or zooming yourself stops following. |
grid |
grid |
'dots' | 'lines' | 'none' |
'dots' |
The background grid. |
gridSize |
grid-size |
number |
24 |
Grid spacing in world units. |
guides |
guides |
boolean |
true |
Line dragged items up with their neighbours, and show guides. |
label |
label |
string |
— | The accessible name. Default: "Canvas". |
lodThreshold |
lod-threshold |
number |
500 |
More items in view than this and the board is painted rather than built from DOM. |
lodZoom |
lod-zoom |
number |
0.4 |
Below this zoom the board is painted rather than built from DOM. |
maxImageBytes |
max-image-bytes |
number |
2_000_000 |
Images dropped or pasted larger than this, in bytes, are refused unless uploadImage is set. |
maxZoom |
max-zoom |
number |
8 |
|
minZoom |
min-zoom |
number |
0.05 |
|
minimap |
minimap |
boolean |
true |
Show the minimap. |
moveThrottle |
move-throttle |
number |
50 |
bmxNodeMove at most this often during a drag, in milliseconds. |
nodes |
nodes |
BmxCanvasNode[] | string |
[] |
The nodes. Setting the list makes the board hold exactly these, with the fewest changes; JSON in markup. |
noteColor |
note-color |
string |
'yellow' |
The colour of new notes and shapes: yellow orange pink violet blue teal green grey, or any CSS colour. |
peers |
peers |
BmxCanvasPeer[] | string |
[] |
Other users on the board. |
penColor |
pen-color |
string |
'' |
The pen's colour: a palette name or any CSS colour. Empty draws in the theme's text colour. |
penWidth |
pen-width |
number |
3 |
The pen's width in world units. The highlighter is five times as wide. |
readonly |
readonly |
boolean |
false |
View only: pan, zoom and select, but change nothing. |
shapeKind |
shape-kind |
BmxCanvasShapeKind |
'rounded' |
The outline the shape tool draws. |
snapToGrid |
snap-to-grid |
boolean |
false |
Snap moved, resized and new items to the grid. Alt held while dragging turns it off. |
tool |
tool |
BmxCanvasTool |
'select' |
What the pointer does. |
toolbar |
toolbar |
boolean |
true |
Show the toolbar. |
uploadImage |
property only | (file: File) => Promise<string> |
— | Turns a dropped or pasted image file into an address the node will show, such as the URL of your upload. Without it, images are kept in the board as data URLs. Set from script; markup cannot hold a function. |
userColor |
user-color |
string |
— | Your colour, shown to the others in a room: any CSS colour. |
userName |
user-name |
string |
— | Your name, shown to the others in a room. |
viewportThrottle |
viewport-throttle |
number |
100 |
bmxViewportChange at most this often. |
wheel |
wheel |
'auto' | 'pan' | 'zoom' |
'auto' |
What the mouse wheel does: auto zooms with a wheel and pans with a trackpad; Ctrl or Cmd always zooms. |
zoomControls |
zoom-controls |
boolean |
true |
Show the zoom buttons. |
Events
| Event | Detail | Description |
|---|---|---|
bmxCanvasStateChange |
BmxCanvasStateChangeDetail |
The board changed here: send detail.ops to the other copies. |
bmxCursorMove |
BmxCanvasCursorDetail |
The pointer moved over the board, or left it. |
bmxFileDrop |
BmxCanvasFileDropDetail |
Files were dropped or pasted. Cancel it to handle them yourself; otherwise images are added. |
bmxNodeActivate |
BmxCanvasActivateDetail |
A node with nothing to edit - an image, embedded content - was opened. |
bmxNodeMove |
BmxCanvasNodeMoveDetail |
Nodes are being dragged or resized. |
bmxPeersChange |
BmxCanvasPeersChangeDetail |
The users in the room changed. |
bmxSelectionChange |
BmxCanvasSelectionDetail |
The selection changed. |
bmxToolChange |
BmxCanvasToolDetail |
The tool changed. |
bmxViewportChange |
BmxCanvasViewportDetail |
The view moved or zoomed. |
Methods
| Method | Signature | Description |
|---|---|---|
addEdges |
addEdges(edges: readonly Partial<BmxCanvasEdge>[] | Partial<BmxCanvasEdge>) => Promise<BmxCanvasEdge[]> |
|
addImage |
addImage(file: Blob, at?: BmxCanvasPoint) => Promise<BmxCanvasNode | null> |
Adds an image from a file or blob, centred on a world point or the view. |
addNodes |
addNodes(nodes: readonly Partial<BmxCanvasNode>[] | Partial<BmxCanvasNode>) => Promise<BmxCanvasNode[]> |
Adds nodes; returns them as stored, with ids given to those without. |
align |
align(how: BmxCanvasAlign, ids?: readonly string[]) => Promise<void> |
Lines nodes up: the selection when none are given. |
applyRemote |
applyRemote(ops: readonly BmxCanvasOp[] | BmxCanvasOp) => Promise<number> |
Applies changes from another copy of the board, as bmxCanvasStateChange or bmxNodeMove gave them there. Returns how many took effect. |
bringToFront |
bringToFront(ids?: readonly string[]) => Promise<void> |
Brings nodes to the front: the selection when none are given. |
centerOn |
centerOn(id: string, animate?: boolean) => Promise<boolean> |
Brings a node to the centre of the view, zooming in if it is too small to read. |
distribute |
distribute(axis: "horizontal" | "vertical", ids?: readonly string[]) => Promise<void> |
Spaces three or more nodes evenly: the selection when none are given. |
exportJson |
exportJson(options?: BmxCanvasExportOptions) => Promise<string> |
The board as JSON text: the snapshot. |
exportPng |
exportPng(options?: BmxCanvasExportOptions) => Promise<Blob> |
The board, or the selection, as a PNG image. Images from other sites appear only if they allow it (CORS). |
exportSvg |
exportSvg(options?: BmxCanvasExportOptions) => Promise<string> |
The board, or the selection, as an SVG document. |
find |
find(text: string) => Promise<string[]> |
The nodes whose text contains text, in reading order. |
followPeer |
followPeer(id: string | null) => Promise<void> |
Follows another user's view, by their peer id; null stops. |
getNode |
getNode(id: string) => Promise<BmxCanvasNode | null> |
A copy of one node. |
getSelection |
getSelection() => Promise<BmxCanvasSelectionDetail> |
|
getSnapshot |
getSnapshot() => Promise<BmxCanvasSnapshot> |
The board as plain data, to save. |
getSyncState |
getSyncState() => Promise<BmxCanvasSyncState> |
The board with every change's stamp: what a client joining late merges with mergeSyncState(). |
getViewport |
getViewport() => Promise<BmxCanvasViewportDetail> |
|
group |
group(ids?: readonly string[]) => Promise<string | null> |
Groups nodes, so they are selected and moved together: the selection when none are given. Returns the group id. |
insertTemplate |
insertTemplate(name: BmxCanvasTemplate) => Promise<string[]> |
Adds a ready-made board - retro, kanban, swot, flowchart or mindmap - where the view is, and shows it. Returns the new ids. |
joinRoom |
joinRoom(room: BmxCanvasTransport | string) => Promise<boolean> |
Shares the board through a room: a channel name, for the other tabs of this browser, or a transport - { send(text), subscribe(listener) } - over a relay that forwards each message to the others. Changes, drags, pointers, selections and views are exchanged, and a copy that joins late is brought up to date. Returns false if the transport is not available. |
leaveRoom |
leaveRoom() => Promise<void> |
Leaves the room, telling the others. |
loadSnapshot |
loadSnapshot(snapshot: BmxCanvasSnapshot) => Promise<void> |
Replaces the board. Sends nothing and clears the undo history. |
mergeSyncState |
mergeSyncState(state: BmxCanvasSyncState) => Promise<boolean> |
Merges another copy's sync state. Returns whether anything changed. |
redo |
redo() => Promise<boolean> |
|
removeEdges |
removeEdges(ids: readonly string[] | string) => Promise<void> |
|
removeNodes |
removeNodes(ids: readonly string[] | string) => Promise<void> |
Removes nodes, and the connectors that end on them. |
rotate |
rotate(degrees: number, ids?: readonly string[]) => Promise<void> |
Turns nodes by degrees, clockwise, about the centre of the box around them: the selection when none are given. Frames and locked nodes stay as they are. To set a node's angle outright, update its rotation. |
screenToWorld |
screenToWorld(clientX: number, clientY: number) => Promise<BmxCanvasPoint> |
A point in the page (clientX, clientY) in world units. |
select |
select(ids: readonly string[] | string) => Promise<void> |
Selects nodes and connectors by id; an empty list clears the selection. |
sendToBack |
sendToBack(ids?: readonly string[]) => Promise<void> |
Sends nodes to the back: the selection when none are given. |
setFocus |
setFocus() => Promise<void> |
Focuses the board. |
setViewport |
setViewport(viewport: Partial<BmxCanvasCamera>, animate?: boolean) => Promise<void> |
Moves the view: x and y are the world point for the top-left corner. |
tidy |
tidy(ids?: readonly string[]) => Promise<void> |
Lays nodes out in a tidy grid: the selection when none are given. |
undo |
undo() => Promise<boolean> |
|
ungroup |
ungroup(ids?: readonly string[]) => Promise<void> |
Takes nodes out of their groups: the selection when none are given. |
updateEdges |
updateEdges(updates: readonly (Partial<BmxCanvasEdge> & { id: string; })[] | (Partial<BmxCanvasEdge> & { id: string; })) => Promise<void> |
|
updateNodes |
updateNodes(updates: readonly (Partial<BmxCanvasNode> & { id: string; })[] | (Partial<BmxCanvasNode> & { id: string; })) => Promise<void> |
Changes nodes: each entry names the node by id and carries the fields to change. |
vote |
vote(id: string, delta?: number) => Promise<{ total: number; mine: number; } | null> |
Adds this user's vote to a node, or takes one away with a negative delta. |
worldToScreen |
worldToScreen(x: number, y: number) => Promise<BmxCanvasPoint> |
A world point as a point in the page. |
zoomIn |
zoomIn() => Promise<void> |
|
zoomOut |
zoomOut() => Promise<void> |
|
zoomTo |
zoomTo(zoom: number, animate?: boolean) => Promise<void> |
Zooms about the centre of the view. |
zoomToFit |
zoomToFit(ids?: readonly string[], animate?: boolean) => Promise<void> |
Frames the whole board, or the nodes given. |
Slots
| Slot | Description |
|---|---|
(named) |
an html node's content: give the element the node's slot name. |
toolbar-end |
extra controls at the end of the toolbar. |
CSS shadow parts
| Part | Description |
|---|---|
arrange-select |
|
context-bar |
|
context-button |
|
drawbar |
|
edge-end |
|
edges |
|
far-layer |
|
following |
|
handle |
|
minimap |
|
port |
|
search |
|
search-count |
|
search-input |
|
selection |
|
shape-select |
|
swatch |
|
templates-item |
|
templates-menu |
|
tool |
|
toolbar |
|
turn-handle |
|
viewport |
|
world |
|
zoom-button |
|
zoom-controls |
CSS custom properties
| Property | Description |
|---|---|
--bmx-infinite-canvas-background |
Behind the board. |
--bmx-infinite-canvas-blue |
The blue note. |
--bmx-infinite-canvas-blue-ink |
Blue lines. |
--bmx-infinite-canvas-edge |
Connectors without a colour of their own. |
--bmx-infinite-canvas-frame |
Inside a frame. |
--bmx-infinite-canvas-green |
The green note. |
--bmx-infinite-canvas-green-ink |
Green lines. |
--bmx-infinite-canvas-grey |
The grey note. |
--bmx-infinite-canvas-grey-ink |
Grey lines. |
--bmx-infinite-canvas-grid |
The grid's dots or lines. |
--bmx-infinite-canvas-height |
How tall the canvas is. |
--bmx-infinite-canvas-laser |
The laser pointer's trail. @part viewport - the board: pan, zoom and everything drawn on it. @part world - the layer that moves and zooms. @part edges - the connectors. @part far-layer - the painted board, zoomed far out. @part selection - the box around the selection. @part handle - a resize handle. @part port - a dot to drag a connector from. @part edge-end - an end of the selected connector, to drag onto another item. @part toolbar - the tools. @part tool - one tool button. @part context-bar - the bar over the selection. @part context-button - one of its buttons. @part swatch - a colour in it. @part shape-select - its shape list. @part zoom-controls - the zoom buttons. @part zoom-button - one of them. @part minimap - the overview in the corner. @part drawbar - the pen, highlighter, eraser and ink colours, while drawing. @part templates-menu - the list of templates. @part templates-item - one template in it. @part arrange-select - the Arrange list over a selection of several items. @part search - the find bar. @part search-input - its field. @part search-count - "2 of 5". @part following - the note saying whose view is being followed. |
--bmx-infinite-canvas-note-text |
Text on notes and coloured shapes, which keep their paper colours on dark themes. |
--bmx-infinite-canvas-orange |
The orange note. |
--bmx-infinite-canvas-orange-ink |
Orange lines. |
--bmx-infinite-canvas-pink |
The pink note. |
--bmx-infinite-canvas-pink-ink |
Pink lines. |
--bmx-infinite-canvas-selection |
Selection outlines, handles and guides. |
--bmx-infinite-canvas-teal |
The teal note. |
--bmx-infinite-canvas-teal-ink |
Teal lines. |
--bmx-infinite-canvas-violet |
The violet note. |
--bmx-infinite-canvas-violet-ink |
Violet lines. |
--bmx-infinite-canvas-yellow |
The yellow note. Also -orange, -pink, -violet, -blue, -teal, -green and -grey. |
--bmx-infinite-canvas-yellow-ink |
Lines and text in yellow: a shape's outline, a connector. Also -orange-ink and so on. |