← All @molecule/* packages · App templates

@molecule/app-canvas-engine-react

Feature · canvas-engine · App (browser) · v1.0.1 · Apache-2.0

Vector design-tool canvas engine — rect/ellipse/line/path/text/group, multi-select + alignment + group/ungroup, snap-to-grid, undo/redo, blend modes. Thin wrapper consuming @molecule/app-feature-canvas-react.

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

npm · Source on GitHub

How it works

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

import { useRef, useState } from 'react'
import {
  CanvasEngine,
  type CanvasDocument,
  type CanvasEngineHandle,
  type CanvasSelection,
} from '@molecule/app-canvas-engine-react'

function Editor() {
  const ref = useRef<CanvasEngineHandle>(null)
  const [doc, setDoc] = useState<CanvasDocument>({
    width: 800,
    height: 600,
    layers: [{ id: 'a', kind: 'rect', x: 40, y: 40, width: 120, height: 80, fill: '#3b82f6' }],
  })
  const [sel, setSel] = useState<CanvasSelection>([])
  return (
    <CanvasEngine
      ref={ref}
      document={doc}
      onChange={setDoc}
      selection={sel}
      onSelectionChange={setSel}
      snapToGrid
      gridSize={8}
    />
  )
}

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

Reference

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.

@molecule/app-canvas-engine-react — vector design-tool canvas engine. Thin domain wrapper on top of @molecule/app-feature-canvas-react (peer dep) that adds:

  • Vector primitives — rect, ellipse, line, path, text, group
  • Style fields — fill, stroke, opacity, blend mode
  • Multi-select — marquee selector + shift/meta/ctrl additive click
  • Alignment — align('left' | 'center' | 'right' | 'top' | 'middle' | 'bottom')
  • Distribution — distribute('horizontal' | 'vertical')
  • Grouping — group() / ungroup()
  • Snap-to-grid — pointer-coords are snapped to gridSize (default 8)
  • Undo/redo — bounded stack (default 100 entries)

The engine never re-implements pan/zoom — that lives in the <CanvasSurface> base.

Exports:

  • <CanvasEngine> — main component + CanvasEngineProps.
  • <VectorElementSvg> — pure-presentational SVG renderer.
  • CanvasEngineHandle — imperative ref API (undo/redo/align/group).
  • CanvasDocument, VectorElement (rect/ellipse/line/path/text/group) and supporting types.
  • alignLayers, distributeLayers — pure layer-list transforms.
  • combinedBounds, elementBounds, rectsIntersect, snapToGrid, translateElement, findElement, unionBounds — geometry helpers.
  • HistoryStack, DEFAULT_HISTORY_LIMIT — bounded undo/redo stack.

Quick Start

import { useRef, useState } from 'react'
import {
  CanvasEngine,
  type CanvasDocument,
  type CanvasEngineHandle,
  type CanvasSelection,
} from '@molecule/app-canvas-engine-react'

function Editor() {
  const ref = useRef<CanvasEngineHandle>(null)
  const [doc, setDoc] = useState<CanvasDocument>({
    width: 800,
    height: 600,
    layers: [{ id: 'a', kind: 'rect', x: 40, y: 40, width: 120, height: 80, fill: '#3b82f6' }],
  })
  const [sel, setSel] = useState<CanvasSelection>([])
  return (
    <CanvasEngine
      ref={ref}
      document={doc}
      onChange={setDoc}
      selection={sel}
      onSelectionChange={setSel}
      snapToGrid
      gridSize={8}
    />
  )
}

Type

feature

Installation

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

API

Interfaces

CanvasDocument

Canvas document — the value model the engine renders. Pure data; no React or DOM dependency. Mutate via onChange (controlled) or rely on the engine's internal state (uncontrolled).

interface CanvasDocument {
  /** Canvas-space document width. */
  width: number
  /** Canvas-space document height. */
  height: number
  /** Top-level layers, in z-order (last paints on top). */
  layers: VectorElement[]
}

CanvasEngineHandle

Imperative handle returned by <CanvasEngine> via ref. Lets the host trigger undo/redo, alignment, grouping, etc. without lifting every action into props.

interface CanvasEngineHandle {
  /** Undo the last document mutation. No-op when stack empty. */
  undo: () => void
  /** Redo a previously-undone mutation. No-op when stack empty. */
  redo: () => void
  /** `true` when there is at least one entry to undo. */
  canUndo: () => boolean
  /** `true` when there is at least one entry to redo. */
  canRedo: () => boolean
  /**
   * Align all currently-selected elements along the requested edge or
   * axis. Operates on top-level layers; group children stay relative
   * to the group.
   */
  align: (mode: CanvasAlignment) => void
  /**
   * Distribute three or more selected elements evenly across the
   * given axis. No-op for fewer than three.
   */
  distribute: (axis: CanvasDistribution) => void
  /** Wrap the current selection in a new group element. */
  group: () => void
  /**
   * Ungroup every selected group element, replacing each with its
   * children at the same z position.
   */
  ungroup: () => void
}

CanvasEngineProps

<CanvasEngine> props.

interface CanvasEngineProps {
  /** Document to render. Treated as controlled when `onChange` is set. */
  document: CanvasDocument
  /**
   * Called whenever the engine mutates the document (drag, align,
   * group, undo, redo). Required for controlled mode; absent in
   * uncontrolled mode.
   */
  onChange?: (next: CanvasDocument) => void
  /** Currently-selected element ids. Treated as controlled when `onSelectionChange` is set. */
  selection?: CanvasSelection
  /** Notified whenever the selection set changes. */
  onSelectionChange?: (next: CanvasSelection) => void
  /** Snap newly-positioned elements to the nearest grid line. Defaults to `true`. */
  snapToGrid?: boolean
  /** Grid spacing in canvas units (visual + snap target). Defaults to `8`. */
  gridSize?: number
  /** Width of the surface in CSS pixels. Defaults to `document.width`. */
  width?: number
  /** Height of the surface in CSS pixels. Defaults to `document.height`. */
  height?: number
  /** Whether to draw the snap grid. Defaults to `true` when snap on. */
  showGrid?: boolean
  /** Optional aria-label override (defaults to a translated label). */
  ariaLabel?: string
  /** Maximum number of undo entries kept. Defaults to 100. */
  historyLimit?: number
  /** Extra classes merged onto the outer surface. */
  className?: string
}

HistoryEntry

Snapshot of the editor state used by the history stack.

interface HistoryEntry {
  /** Document snapshot at this point in time. */
  document: CanvasDocument
}

VectorElementBase

Common fields on every {@link VectorElement} variant.

interface VectorElementBase extends VectorStyle {
  /** Stable id for selection / history / React keys. */
  id: VectorElementId
  /** Optional transform around the element centre. */
  transform?: VectorTransform
}

VectorElementSvgProps

<VectorElementSvg> props.

interface VectorElementSvgProps {
  /** Element to draw. */
  element: VectorElement
  /** Whether the element is currently selected (drives `data-selected`). */
  selected?: boolean
}

VectorEllipse

Axis-aligned ellipse, defined by bounding-box.

interface VectorEllipse extends VectorElementBase {
  /** Discriminator. */
  kind: 'ellipse'
  /** Canvas-space x of the bounding-box top-left corner. */
  x: number
  /** Canvas-space y of the bounding-box top-left corner. */
  y: number
  /** Bounding-box width in canvas units. */
  width: number
  /** Bounding-box height in canvas units. */
  height: number
}

VectorGroup

Group element — a logical bundle of children rendered together. The group's x/y/width/height is its child bounding-box snapshot (used for alignment ops); children remain in their absolute canvas-space coordinates so ungrouping is trivial.

interface VectorGroup extends VectorElementBase {
  /** Discriminator. */
  kind: 'group'
  /** Canvas-space x of the children's combined bounding-box. */
  x: number
  /** Canvas-space y of the children's combined bounding-box. */
  y: number
  /** Combined bounding-box width. */
  width: number
  /** Combined bounding-box height. */
  height: number
  /** Group children, in z-order (last paints on top). */
  children: VectorElement[]
}

VectorLine

Straight line between two canvas-space points.

interface VectorLine extends VectorElementBase {
  /** Discriminator. */
  kind: 'line'
  /** Canvas-space start x. */
  x1: number
  /** Canvas-space start y. */
  y1: number
  /** Canvas-space end x. */
  x2: number
  /** Canvas-space end y. */
  y2: number
}

VectorPath

Free-form SVG path.

interface VectorPath extends VectorElementBase {
  /** Discriminator. */
  kind: 'path'
  /** Canvas-space x of the path's bounding-box top-left. */
  x: number
  /** Canvas-space y of the path's bounding-box top-left. */
  y: number
  /** Canvas-space bounding-box width — used for alignment helpers. */
  width: number
  /** Canvas-space bounding-box height — used for alignment helpers. */
  height: number
  /** SVG `d` attribute, expressed in element-local coordinates. */
  d: string
}

VectorRect

Axis-aligned rectangle.

interface VectorRect extends VectorElementBase {
  /** Discriminator. */
  kind: 'rect'
  /** Canvas-space x of the top-left corner. */
  x: number
  /** Canvas-space y of the top-left corner. */
  y: number
  /** Canvas-space width. */
  width: number
  /** Canvas-space height. */
  height: number
  /** Optional corner radius in canvas units. */
  cornerRadius?: number
}

VectorStyle

Style fields shared by every visual element kind.

interface VectorStyle {
  /** Fill colour as any CSS colour string. Omit for transparent. */
  fill?: string
  /** Stroke colour as any CSS colour string. Omit for no stroke. */
  stroke?: string
  /** Stroke width in canvas units. Defaults to `1` when stroke set. */
  strokeWidth?: number
  /** Element opacity, `0`..`1`. Defaults to `1`. */
  opacity?: number
  /** CSS-compatible blend mode. Defaults to `'normal'`. */
  blendMode?: VectorBlendMode
}

VectorText

Text element rendered as an SVG <text>.

interface VectorText extends VectorElementBase {
  /** Discriminator. */
  kind: 'text'
  /** Canvas-space x of the text's bounding-box top-left. */
  x: number
  /** Canvas-space y of the text's bounding-box top-left. */
  y: number
  /** Bounding-box width in canvas units (used for alignment). */
  width: number
  /** Bounding-box height in canvas units (used for alignment). */
  height: number
  /** Plain string contents. */
  text: string
  /** Font size in canvas units. Defaults to `16`. */
  fontSize?: number
  /** CSS font-family stack. Defaults to system-ui. */
  fontFamily?: string
  /** Font weight. Defaults to `'normal'`. */
  fontWeight?: 'normal' | 'bold' | number
}

VectorTransform

Affine 2D transform applied to a {@link VectorElement} relative to its position. All fields are optional; omit for identity.

interface VectorTransform {
  /** Rotation in degrees, clockwise around the element centre. */
  rotation?: number
  /** Uniform or x-axis scale (1 = identity). */
  scaleX?: number
  /** Y-axis scale (1 = identity). */
  scaleY?: number
}

Types

CanvasAlignment

Alignment ops the engine exposes via {@link CanvasEngineHandle}.

type CanvasAlignment = 'left' | 'center' | 'right' | 'top' | 'middle' | 'bottom'

CanvasDistribution

Distribution ops the engine exposes via {@link CanvasEngineHandle}.

type CanvasDistribution = 'horizontal' | 'vertical'

CanvasSelection

Set-shaped selection of element ids. We use an array on the wire so the type is JSON-serialisable (history, persistence, undo/redo).

type CanvasSelection = readonly VectorElementId[]

VectorBlendMode

CSS-compatible blend mode names supported by the engine.

type VectorBlendMode =
  | 'normal'
  | 'multiply'
  | 'screen'
  | 'overlay'
  | 'darken'
  | 'lighten'
  | 'color-dodge'
  | 'color-burn'
  | 'hard-light'
  | 'soft-light'
  | 'difference'
  | 'exclusion'
  | 'hue'
  | 'saturation'
  | 'color'
  | 'luminosity'

VectorElement

Discriminated union of every supported element kind.

type VectorElement = VectorRect | VectorEllipse | VectorLine | VectorPath | VectorText | VectorGroup

VectorElementId

Stable identifier for a {@link VectorElement}.

type VectorElementId = string

Classes

HistoryStack

Bounded LIFO + LIFO pair backing undo/redo. State is intentionally mutable so React refs can wrap it — every public method either pushes/pops a snapshot or queries stack depth.

Functions

alignLayers(layers, selection, mode)

Apply an alignment op to every selected layer, returning the new layers array. Layers not in the selection are passed through.

function alignLayers(
  layers: readonly VectorElement[],
  selection: CanvasSelection,
  mode: CanvasAlignment,
): VectorElement[]
  • layers — Top-level layer list.
  • selection — Ids of layers to operate on.
  • mode — Alignment mode.

Returns: New layer list with aligned elements.

combinedBounds(elements)

Compute the bounding box that contains every element in the list. Returns a zero-sized rect at the origin when elements is empty so downstream code can blindly read the result.

function combinedBounds(elements: readonly VectorElement[]): Bounds
  • elements — Non-empty list of elements.

Returns: Combined bounding box.

distributeLayers(layers, selection, axis)

Distribute three or more selected layers evenly across the given axis between the outermost layers (which stay put). Returns a new layer list. For fewer than three selected items the input is returned unchanged.

function distributeLayers(
  layers: readonly VectorElement[],
  selection: CanvasSelection,
  axis: CanvasDistribution,
): VectorElement[]
  • layers — Top-level layer list.
  • selection — Ids of layers to operate on.
  • axis — Distribution axis.

Returns: New layer list with distributed elements.

elementBounds(element)

Compute the axis-aligned bounding box of a single element. Lines use their two endpoints; every other kind uses its x/y/width/height.

function elementBounds(element: VectorElement): Bounds
  • element — Element to measure.

Returns: Bounding box in canvas-space coordinates.

findElement(elements, id)

Look up an element by id within a flat list. Returns undefined when not found — callers decide whether that's an error.

function findElement(elements: readonly VectorElement[], id: string): VectorElement | undefined
  • elements — Source list.
  • id — Element id.

Returns: The matching element or undefined.

rectsIntersect(bounds, box)

true when bounds and box overlap on both axes (touching edges count as overlap). Used by the marquee selector.

function rectsIntersect(bounds: Bounds, box: Bounds): boolean
  • bounds — First rectangle.
  • box — Second rectangle.

Returns: Whether the rects intersect.

snapToGrid(value, gridSize)

Snap a value to the nearest multiple of gridSize. When gridSize is <= 0 the input is returned unchanged so callers don't have to branch on "snap disabled".

function snapToGrid(value: number, gridSize: number): number
  • value — Value to snap (canvas units).
  • gridSize — Grid spacing in canvas units. <= 0 disables snap.

Returns: The snapped value.

translateElement(element, dx, dy)

Translate an element by (dx, dy) in canvas-space, returning a new element. Group children are translated recursively so the group stays internally consistent.

function translateElement(element: VectorElement, dx: number, dy: number): VectorElement
  • element — Source element.
  • dx — X-axis displacement in canvas units.
  • dy — Y-axis displacement in canvas units.

Returns: Translated element (new object).

unionBounds(a, b)

Combine two bounding boxes into the smallest axis-aligned rect that contains both. Useful for selection envelopes and group snapshots.

function unionBounds(a: Bounds, b: Bounds): Bounds
  • a — First bounding box.
  • b — Second bounding box.

Returns: The union rectangle.

VectorElementSvg(props)

Render a single vector element as SVG. Group children render recursively. The element is wrapped in a <g> so callers can attach data attributes consistently.

function VectorElementSvg(
  props: VectorElementSvgProps,
): ReactElement<unknown, string | JSXElementConstructor<any>>
  • props — Component props.

Returns: The rendered SVG element.

Constants

CanvasEngine

Vector design-tool canvas engine. Read the module-level docstring for the high-level behaviour summary.

const CanvasEngine: ForwardRefExoticComponent<CanvasEngineProps & RefAttributes<CanvasEngineHandle>>

DEFAULT_HISTORY_LIMIT

Default cap matches the design spec (100 entries).

const DEFAULT_HISTORY_LIMIT: 100

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

Translations

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