← All @molecule/* packages · App templates
@molecule/app-canvas-engine-reactFeature · 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@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
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.tsJSDoc, 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:
rect, ellipse, line, path, text, groupalign('left' | 'center' | 'right' | 'top' | 'middle' | 'bottom')distribute('horizontal' | 'vertical')group() / ungroup()gridSize (default 8)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.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}
/>
)
}
feature
npm install @molecule/app-canvas-engine-react @molecule/app-feature-canvas-react @molecule/app-react @molecule/app-ui react
npm install -D @types/react
CanvasDocumentCanvas 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[]
}
CanvasEngineHandleImperative 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
}
HistoryEntrySnapshot of the editor state used by the history stack.
interface HistoryEntry {
/** Document snapshot at this point in time. */
document: CanvasDocument
}
VectorElementBaseCommon 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
}
VectorEllipseAxis-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
}
VectorGroupGroup 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[]
}
VectorLineStraight 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
}
VectorPathFree-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
}
VectorRectAxis-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
}
VectorStyleStyle 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
}
VectorTextText 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
}
VectorTransformAffine 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
}
CanvasAlignmentAlignment ops the engine exposes via {@link CanvasEngineHandle}.
type CanvasAlignment = 'left' | 'center' | 'right' | 'top' | 'middle' | 'bottom'
CanvasDistributionDistribution ops the engine exposes via {@link CanvasEngineHandle}.
type CanvasDistribution = 'horizontal' | 'vertical'
CanvasSelectionSet-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[]
VectorBlendModeCSS-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'
VectorElementDiscriminated union of every supported element kind.
type VectorElement = VectorRect | VectorEllipse | VectorLine | VectorPath | VectorText | VectorGroup
VectorElementIdStable identifier for a {@link VectorElement}.
type VectorElementId = string
HistoryStackBounded 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.
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.
CanvasEngineVector design-tool canvas engine. Read the module-level docstring for the high-level behaviour summary.
const CanvasEngine: ForwardRefExoticComponent<CanvasEngineProps & RefAttributes<CanvasEngineHandle>>
DEFAULT_HISTORY_LIMITDefault cap matches the design spec (100 entries).
const DEFAULT_HISTORY_LIMIT: 100
Peer dependencies:
@molecule/app-feature-canvas-react ^1.0.1@molecule/app-react ^1.0.1@molecule/app-ui ^1.0.1react ^18.0.0 || ^19.0.0@molecule/app-feature-canvas-react@molecule/app-react@molecule/app-uireactTranslation strings are provided by @molecule/app-locales-canvas-engine.