v1.6.0

<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

Add a note Show everything Undo
Drag, type, connect. Double-click the board for a note, drag from the dots round an item to connect it, and press Delete 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.
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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
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

SlotDescription
(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

PartDescription
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

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