@molecule/app-whiteboard-canvas-react

Feature · whiteboard-canvas · App (browser) · v1.0.2 · Apache-2.0

React whiteboard canvas — pen / marker / eraser / sticky-note / line / arrow / shape / text tools layered on top of @molecule/app-feature-canvas-react

npm install @molecule/app-whiteboard-canvas-react

npm · Source on GitHub

How it works

@molecule/app-whiteboard-canvas-react is a ready-made whiteboard-canvas feature for the app (browser) side. It composes the core interfaces it needs, so it works with whichever providers your app has bonded.

import { useState } from 'react'

import {
  WhiteboardCanvas,
  type WhiteboardShape,
  type WhiteboardStickyNote,
  type WhiteboardStroke,
  type WhiteboardTool,
} from '@molecule/app-whiteboard-canvas-react'

function Demo() {
  const [tool, setTool] = useState<WhiteboardTool>('pen')
  const [strokes, setStrokes] = useState<WhiteboardStroke[]>([])
  const [shapes, setShapes] = useState<WhiteboardShape[]>([])
  const [stickyNotes, setStickyNotes] = useState<WhiteboardStickyNote[]>([])
  return (
    <WhiteboardCanvas
      tool={tool}
      strokes={strokes}
      shapes={shapes}
      stickyNotes={stickyNotes}
      onChange={(c) => {
        setStrokes(c.strokes)
        setShapes(c.shapes)
        setStickyNotes(c.stickyNotes)
      }}
      width={800}
      height={600}
    />
  )
}

Works with: @molecule/app-feature-canvas-react, @molecule/app-react, @molecule/app-ui

Reference

React whiteboard canvas — pen / marker / eraser / sticky-note / line / arrow / shape / text tools layered on top of @molecule/app-feature-canvas-react.

Pan + zoom infrastructure is delegated to <CanvasSurface> from the base — this package contributes only the whiteboard-specific tools, geometry helpers, and element types (free-form strokes, vector shapes, sticky notes / text boxes).

Exports:

  • <WhiteboardCanvas> — controlled canvas component. Accepts strokes, shapes, stickyNotes, tool, and an onChange callback fired at the end of every gesture.
  • WhiteboardSnapshot, WhiteboardStroke, WhiteboardShape, WhiteboardStickyNote, WhiteboardTool types describing the board's element model.
  • buildStrokePath, buildShapePath, shapeBounds, applyEraserStrokes, strokeIntersectsPath, segmentsIntersect, defaultStrokeColor, defaultStrokeWidth, defaultShapeStyle, defaultStickyNoteStyle, generateWhiteboardId — pure geometry / id helpers (no React, no DOM).

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { useState } from 'react'

import {
  WhiteboardCanvas,
  type WhiteboardShape,
  type WhiteboardStickyNote,
  type WhiteboardStroke,
  type WhiteboardTool,
} from '@molecule/app-whiteboard-canvas-react'

function Demo() {
  const [tool, setTool] = useState<WhiteboardTool>('pen')
  const [strokes, setStrokes] = useState<WhiteboardStroke[]>([])
  const [shapes, setShapes] = useState<WhiteboardShape[]>([])
  const [stickyNotes, setStickyNotes] = useState<WhiteboardStickyNote[]>([])
  return (
    <WhiteboardCanvas
      tool={tool}
      strokes={strokes}
      shapes={shapes}
      stickyNotes={stickyNotes}
      onChange={(c) => {
        setStrokes(c.strokes)
        setShapes(c.shapes)
        setStickyNotes(c.stickyNotes)
      }}
      width={800}
      height={600}
    />
  )
}

Type

feature

Installation

npm install @molecule/app-whiteboard-canvas-react @molecule/app-feature-canvas-react @molecule/app-react @molecule/app-ui react
npm install -D @types/react

API

Interfaces

WhiteboardChange

onChange payload for WhiteboardCanvas.

interface WhiteboardChange {
  /** Updated stroke list (controlled). */
  strokes: WhiteboardStroke[]
  /** Updated shape list (controlled). */
  shapes: WhiteboardShape[]
  /** Updated sticky-note list (controlled). */
  stickyNotes: WhiteboardStickyNote[]
}

WhiteboardCanvasProps

WhiteboardCanvas props.

interface WhiteboardCanvasProps {
  /**
   * Free-form strokes (pen / marker / eraser) currently on the board.
   * Controlled — feed back through `onChange`.
   */
  strokes: WhiteboardStroke[]
  /** Vector shapes on the board. Controlled. */
  shapes: WhiteboardShape[]
  /** Sticky notes / text boxes on the board. Controlled. */
  stickyNotes: WhiteboardStickyNote[]
  /**
   * Called whenever the user finishes a tool gesture that mutates the
   * board (stroke ended, shape committed, sticky placed). The payload
   * is the COMPLETE post-gesture state — replace your local arrays
   * with it.
   */
  onChange: (change: WhiteboardChange) => void
  /** Active drawing tool. */
  tool: WhiteboardTool
  /**
   * Optional override for in-flight stroke color (pen / marker only).
   * If omitted, uses `defaultStrokeColor` for the tool.
   */
  strokeColor?: string
  /** Optional override for stroke width. */
  strokeWidth?: number
  /**
   * Optional sticky-note background color override (used by the
   * sticky tool when placing a new note).
   */
  stickyBackground?: string
  /** Surface width in CSS pixels. */
  width: number
  /** Surface height in CSS pixels. */
  height: number
  /** Optional initial viewport (uncontrolled CanvasSurface viewport). */
  initialViewport?: CanvasViewport
  /** Optional viewport clamping limits forwarded to `<CanvasSurface>`. */
  viewportLimits?: ViewportLimits
  /**
   * If true, every drawing tool is disabled (background pan still
   * works). Useful for read-only viewers / playback mode.
   */
  readOnly?: boolean
  /** Optional aria-label override (defaults to a translated label). */
  ariaLabel?: string
  /** Extra classes merged onto the wrapper. */
  className?: string
}

WhiteboardStroke

A free-form stroke (pen / marker / eraser). points is the raw sampled pointer trail in canvas-space; rendering smooths the path via quadratic Bezier midpoints. The eraser kind is itself a stroke (so it round-trips through onChange) — consumers use applyEraserStrokes to diff erasers against earlier strokes when persisting.

interface WhiteboardStroke {
  /** Stable id for selection / undo / realtime broadcast. */
  id: string
  /** Tool that produced the stroke. */
  kind: WhiteboardStrokeKind
  /** Raw sampled points in canvas-space. */
  points: Point[]
  /** CSS color string. Defaults are tool-specific. */
  color: string
  /** Stroke width in canvas units. Marker uses a fixed width. */
  width: number
}

WhiteboardShape

A vector shape defined by two canvas-space corner points (from, to). rect and ellipse derive their bounding rect from the two points; line and arrow render as straight segments.

interface WhiteboardShape {
  /** Stable id for selection / undo / realtime broadcast. */
  id: string
  /** Geometry kind. */
  kind: WhiteboardShapeKind
  /** Canvas-space start point. */
  from: Point
  /** Canvas-space end point. */
  to: Point
  /** Stroke color. */
  stroke: string
  /** Stroke width in canvas units. */
  strokeWidth: number
  /** Optional fill (rect / ellipse only). */
  fill?: string
}

WhiteboardStickyNote

A sticky note / text box positioned in canvas-space. width / height are canvas-space; text is the user-entered content. Plain text tool uses the same shape but with a transparent background.

interface WhiteboardStickyNote {
  /** Stable id for selection / undo / realtime broadcast. */
  id: string
  /** Canvas-space top-left. */
  position: Point
  /** Canvas-space size. */
  width: number
  /** Canvas-space size. */
  height: number
  /** Note body text. */
  text: string
  /** Background fill. Use a transparent CSS color for plain text. */
  background: string
  /** Text color. */
  color: string
}

WhiteboardSnapshot

Aggregate of every element kind on the board. This is what persistence layers serialize, what realtime broadcasts mirror, and what WhiteboardCanvas consumes via strokes, shapes, and stickyNotes props (controlled).

interface WhiteboardSnapshot {
  /** All free-form strokes (pen / marker / eraser) in chronological order. */
  strokes: WhiteboardStroke[]
  /** All vector shapes. */
  shapes: WhiteboardShape[]
  /** All sticky notes / text boxes. */
  stickyNotes: WhiteboardStickyNote[]
}

Types

WhiteboardStrokeKind

Stroke produced by the pen tool — Bezier-smoothed free-form path.

type WhiteboardStrokeKind = 'pen' | 'marker' | 'eraser'

WhiteboardShapeKind

Vector shape kinds the whiteboard knows how to draw.

type WhiteboardShapeKind = 'line' | 'arrow' | 'rect' | 'ellipse'

WhiteboardTool

The active tool. select is a no-op pass-through for the canvas — consumers can build their own selection / move logic on top.

type WhiteboardTool =
  'pen' | 'marker' | 'eraser' | 'sticky' | 'line' | 'arrow' | 'rect' | 'ellipse' | 'text' | 'select'

Functions

WhiteboardCanvas(props)

React whiteboard canvas — pen / marker / eraser / sticky-note / line / arrow / shape / text tools layered on top of CanvasSurface from @molecule/app-feature-canvas-react.

Pan + zoom are delegated entirely to the base — this component only handles tool-specific pointer-event interpretation and rendering of strokes, shapes, and sticky notes inside the canvas-space inner layer. All styling goes through getClassMap(); all user-facing text goes through t() — both per molecule architecture rules.

Controlled state model: pass strokes / shapes / stickyNotes and listen for onChange. Each completed gesture fires onChange with the full post-gesture state.

function WhiteboardCanvas(props: WhiteboardCanvasProps): JSX.Element
  • props — Component props.

Returns: The whiteboard canvas element.

generateWhiteboardId()

Generate an id unique enough for in-process selection + realtime broadcast. Not cryptographic. Falls back gracefully when crypto.randomUUID is unavailable (older Node, jsdom).

function generateWhiteboardId(): string

Returns: A short unique id string.

buildStrokePath(points)

Build an SVG path string for a free-form stroke using quadratic Bezier midpoints — the standard "smooth pen" technique. Each intermediate point becomes a control point and the midpoint between adjacent points becomes the curve endpoint, which produces a smoothly-tangent path even from sparse pointer samples.

Returns an empty string for zero-point strokes and a single M-command for one-point strokes (so renderers can still emit a dot).

function buildStrokePath(points: readonly Point[]): string
  • points — Raw sampled canvas-space points.

Returns: An SVG path data string.

defaultStrokeWidth(kind)

Default stroke width in canvas units for a given tool. Marker is intentionally fixed to communicate "highlighter"; pen / eraser are thinner / wider.

function defaultStrokeWidth(kind: WhiteboardStrokeKind): number
  • kind — Stroke tool kind.

Returns: Default canvas-space stroke width.

defaultStrokeColor(kind)

Default color for a given stroke tool.

function defaultStrokeColor(kind: WhiteboardStrokeKind): string
  • kind — Stroke tool kind.

Returns: A CSS color string.

shapeBounds(shape)

Compute the axis-aligned bounding rect of a vector shape's two canvas-space corner points. Useful for hit-testing and rendering rect / ellipse.

function shapeBounds(shape: WhiteboardShape): {
  x: number
  y: number
  width: number
  height: number
}
  • shape — The vector shape.

Returns: { x, y, width, height } in canvas-space.

buildShapePath(shape)

Build SVG path data for a vector shape — straight segment for line, segment with a small arrowhead for arrow. rect and ellipse are typically rendered as their own SVG elements; this helper still returns a closed path for them so consumers can fall back to <path> if desired.

function buildShapePath(shape: WhiteboardShape): string
  • shape — The vector shape.

Returns: An SVG path data string.

defaultShapeStyle(kind)

Default fill / stroke palette per shape kind. Consumers override via direct prop assignment; this is just the initial-tap default.

function defaultShapeStyle(kind: WhiteboardShapeKind): {
  stroke: string
  strokeWidth: number
  fill?: string
}
  • kind — Shape kind.

Returns: { stroke, strokeWidth, fill } defaults.

defaultStickyNoteStyle()

Default sticky-note size + colors. Plain text-tool consumers can override background to a transparent value.

function defaultStickyNoteStyle(): {
  width: number
  height: number
  background: string
  color: string
}

Returns: Default sticky-note metrics + palette.

segmentsIntersect(a1, a2, b1, b2)

Test whether two segments intersect using the standard CCW orientation test. Used by strokeIntersectsPath for eraser hit-testing against existing strokes.

Collinear endpoints only count when the bounding boxes also overlap — otherwise two parallel-but-distant segments would register as touching just because every CCW determinant is zero.

function segmentsIntersect(a1: Point, a2: Point, b1: Point, b2: Point): boolean
  • a1 — First segment start.
  • a2 — First segment end.
  • b1 — Second segment start.
  • b2 — Second segment end.

Returns: true if the segments cross or share a touching point.

strokeIntersectsPath(eraser, target)

true if the eraser stroke eraser crosses any segment of target within tolerance canvas units. Used by applyEraserStrokes to diff erasers against earlier strokes.

function strokeIntersectsPath(eraser: WhiteboardStroke, target: WhiteboardStroke): boolean
  • eraser — Eraser stroke (path of points).
  • target — Target stroke to test against.

Returns: true when the two paths cross.

applyEraserStrokes(strokes)

Apply every eraser stroke in strokes against the non-eraser strokes that came before it. Returns a new array containing only the surviving non-eraser strokes — useful when persisting a snapshot or compacting an undo history.

Erasers themselves are dropped from the result; consumers that need to keep them for replay should diff manually.

function applyEraserStrokes(strokes: readonly WhiteboardStroke[]): WhiteboardStroke[]
  • strokes — The full chronological stroke list.

Returns: Surviving non-eraser strokes (eraser entries removed).

Injection Notes

Requirements

Peer dependencies:

  • @molecule/app-feature-canvas-react ^1.0.1
  • @molecule/app-react ^1.0.1
  • @molecule/app-ui ^1.0.1
  • react ^18.0.0 || ^19.0.0

Runtime Dependencies

  • @molecule/app-feature-canvas-react
  • @molecule/app-react
  • @molecule/app-ui
  • react

width / height are REQUIRED CSS-pixel numbers — the surface does not auto-size to its container; measure the container (e.g. a ResizeObserver) for fill layouts. Requires the @molecule/app-feature-canvas-react peer (pan/zoom base). Eraser gestures round-trip through onChange as strokes; run applyEraserStrokes before persisting if you want erasures baked in. UI strings come from the companion @molecule/app-locales-whiteboard-canvas bond. readOnly disables all drawing tools but keeps background pan.

Translations

Translation strings are provided by @molecule/app-locales-whiteboard-canvas.