Skip to main content

The CanvasEngine

CanvasEngine is the central object in @rifrocket/fabricjs-design-tool. It owns one Fabric.js Canvas and composes narrow, independently-testable managers around it instead of exposing the raw canvas as the primary surface.

import { createEngine } from "@rifrocket/fabricjs-design-tool";

const engine = createEngine(canvasElement, { width: 800, height: 600 });

createEngine() is a thin static factory — CanvasEngine.create() under the hood — that constructs the Fabric Canvas and every manager below it in one call.

The managers

ManagerAccessed viaResponsibility
ViewportManagerengine.viewportZoom/pan math
SelectionManagerengine.selectionActive object(s), group/ungroup
LayerManagerengine.layersZ-order, visibility, lock state
AlignmentManagerengine.alignmentAlign/distribute
SnapEngineengine.snappingSmart-guide snapping
HistoryManagerengine.historyCommand-pattern undo/redo
PluginRegistryengine.registryObject types, tools, panels, effects, exporters, importers
Storeengine.storeObservable state (see Store & Events)
EventBusengine.eventsPub-sub with middleware
KeyboardShortcutManagerengine.shortcutsKeyboard shortcut registration

Each manager only knows about the raw Fabric Canvas it was constructed with — none of them know about each other, and none of them know about CanvasEngine itself. CanvasEngine's own methods are largely facades that call a manager and then keep the reactive Store in sync, which matters more than it sounds:

// Prefer this — keeps EngineState.zoom (and anything subscribed to it) in sync:
engine.setZoom(1.5);

// Not this — mutates the canvas correctly, but the store never finds out:
engine.viewport.setZoom(1.5); // marked @internal in JSDoc for exactly this reason

ViewportManager's raw setZoom/zoomBy/pan/panTo/reset methods are marked @internal for this reason — always prefer the CanvasEngine facade method (engine.setZoom(), engine.zoomBy(), engine.reset()) unless you have a specific reason not to.

Creating and adding objects

Object creation goes through the object-type registry rather than importing Fabric classes directly:

// Creates and adds through the history-tracked path (undoable):
const rect = await engine.addObjectOfType("rect", { left: 10, top: 10, width: 100, height: 60 });

// Creates without adding to the canvas or history:
const rect = await engine.createObject("rect", { left: 10, top: 10 });

create() on an object-type definition is always async and always awaited — real object types need it (QR codes generate asynchronously, image objects decode a URL, SVG import parses a document), so every type goes through the same async contract regardless of whether a given type actually needs it.

Mutating properties, deleting, undo/redo

engine.setObjectProperty(rect, "fill", "#ff0000"); // undoable
engine.removeObject(rect); // undoable
engine.deleteSelection(); // removes every active object as one undo step
engine.undo();
engine.redo();

See History & Commands for why these are safe to call in rapid succession (e.g. from a slider) without flooding the undo stack with one entry per pixel.

Selection and grouping

engine.selection.getActive(); // FabricObject | undefined
engine.selection.getActiveObjects(); // FabricObject[]
engine.selection.select(object);
engine.selection.selectMultiple([a, b, c]);
engine.selection.group(); // returns the new Group, or null if < 2 objects selected
engine.selection.ungroup(); // returns the ungrouped objects, or null if the active object isn't a Group

Layers

engine.layers.bringToFront(object);
engine.layers.sendToBack(object);
engine.layers.bringForward(object);
engine.layers.sendBackward(object);
engine.layers.moveToIndex(object, 2); // for drag-to-reorder UI
engine.layers.setVisible(object, false);
engine.layers.setLocked(object, true); // locks movement/rotation/scaling, clears selectable

Alignment and distribution

engine.alignment.align("left"); // "left" | "center" | "right" | "top" | "middle" | "bottom"
engine.alignment.distribute("horizontal"); // "horizontal" | "vertical"

With a single object selected, align() aligns to the canvas bounds. With multiple objects selected, it aligns to the selection's own bounding box instead. distribute() equalizes the gap between adjacent object edges along an axis, anchoring on the first and last objects by position.

Snapping

engine.snapping.setEnabled(true);
engine.snapping.isEnabled();
engine.snapping.setOptions({ threshold: 8, lineColor: "#32D10A" });

Snapping is disabled by default in <DesignEditor>'s built-in presets — an earlier version of this project shipped it default-on with unfiltered guide rendering, which made it actively disruptive rather than helpful. Turn it on explicitly once you want it.

Import/export

const result = engine.export("png"); // { format, fileName, mimeType, data }
await engine.importFile("json", jsonString);

Both go through the same exporters/importers registries a plugin extends with a custom format — engine.export() isn't special-cased for the 5 built-in formats (png, jpeg, svg, json, and whatever plugin-export-pdf adds). See Export/Import Pipelines.

The escape hatch

engine.getFabricCanvas() returns the raw Fabric Canvas — an explicit, documented-as-unstable escape hatch for anything the manager API doesn't cover yet. Reach for it deliberately, not as a first instinct; mutations made directly on the raw canvas bypass history tracking and the reactive store, so UI that reads from engine.store won't notice the change.