@bpmnkit/canvas 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,177 @@
1
+ import type { BpmnDefinitions, BpmnDiEdge, BpmnDiShape, BpmnFlowElement, BpmnTextAnnotation } from "@bpmnkit/core";
2
+ /** The color theme applied to the canvas. */
3
+ export type Theme = "light" | "dark" | "auto";
4
+ /**
5
+ * Controls how the diagram is initially positioned in the viewport.
6
+ * - `"contain"` — scale and center the diagram to fill the available space (default)
7
+ * - `"center"` — center without scaling
8
+ * - `"none"` — use the diagram's raw coordinates unchanged
9
+ */
10
+ export type FitMode = "contain" | "center" | "none";
11
+ /** Configuration options for {@link BpmnCanvas}. */
12
+ export interface CanvasOptions {
13
+ /** The DOM element to mount the canvas into. */
14
+ container: HTMLElement;
15
+ /** BPMN 2.0 XML to render immediately. Can also be provided later via {@link BpmnCanvas.load}. */
16
+ xml?: string;
17
+ /**
18
+ * Color theme. Use `"auto"` to follow the OS preference (prefers-color-scheme).
19
+ * @default "auto"
20
+ */
21
+ theme?: Theme;
22
+ /**
23
+ * Show a dot-grid background on the infinite canvas.
24
+ * @default true
25
+ */
26
+ grid?: boolean;
27
+ /**
28
+ * How to position the diagram when first rendered.
29
+ * @default "contain"
30
+ */
31
+ fit?: FitMode;
32
+ /**
33
+ * Plugins to install. Each plugin receives a {@link CanvasApi} handle and
34
+ * can extend the canvas with editing, overlays, tooltips, or custom shapes.
35
+ * @see {@link CanvasPlugin}
36
+ */
37
+ plugins?: CanvasPlugin[];
38
+ }
39
+ /** The current pan/zoom state of the canvas viewport. */
40
+ export interface ViewportState {
41
+ /** Horizontal translation in screen pixels. */
42
+ tx: number;
43
+ /** Vertical translation in screen pixels. */
44
+ ty: number;
45
+ /** Zoom scale factor. `1.0` = 100%, `0.5` = 50%, `2.0` = 200%. */
46
+ scale: number;
47
+ }
48
+ /** A rendered BPMN shape with its SVG element and source model data. */
49
+ export interface RenderedShape {
50
+ /** The BPMN element ID. */
51
+ readonly id: string;
52
+ /** The SVG `<g>` element. */
53
+ readonly element: SVGGElement;
54
+ /** DI shape data — contains position and size. */
55
+ readonly shape: BpmnDiShape;
56
+ /** The matching BPMN flow element from the process model, if found. */
57
+ readonly flowElement: BpmnFlowElement | undefined;
58
+ /** Set if this shape represents a text annotation. */
59
+ readonly annotation?: BpmnTextAnnotation;
60
+ }
61
+ /** A rendered BPMN edge (sequence flow or association) with its SVG element. */
62
+ export interface RenderedEdge {
63
+ /** The BPMN element ID. */
64
+ readonly id: string;
65
+ /** The SVG `<g>` element. */
66
+ readonly element: SVGGElement;
67
+ /** DI edge data — contains waypoints and optional label bounds. */
68
+ readonly edge: BpmnDiEdge;
69
+ }
70
+ /** Events emitted by {@link BpmnCanvas}. */
71
+ export interface CanvasEvents {
72
+ /** Fired whenever the viewport is panned or zoomed. */
73
+ "viewport:change": (state: ViewportState) => void;
74
+ /** Fired when a BPMN element is clicked. */
75
+ "element:click": (id: string, event: PointerEvent) => void;
76
+ /** Fired when keyboard focus moves to a BPMN element. */
77
+ "element:focus": (id: string) => void;
78
+ /** Fired when keyboard focus leaves all BPMN elements. */
79
+ "element:blur": () => void;
80
+ /** Fired after a BPMN diagram is loaded and rendered. */
81
+ "diagram:load": (defs: BpmnDefinitions) => void;
82
+ /** Fired when the canvas is cleared. */
83
+ "diagram:clear": () => void;
84
+ }
85
+ /**
86
+ * The stable API surface exposed to plugins.
87
+ *
88
+ * Plugins receive a `CanvasApi` instance in their `install` method and use it
89
+ * to observe and interact with the canvas without accessing internals.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * const hoverPlugin: CanvasPlugin = {
94
+ * name: "hover-highlight",
95
+ * install(api) {
96
+ * api.on("element:click", (id) => {
97
+ * const shape = api.getShapes().find((s) => s.id === id);
98
+ * console.log("Clicked:", shape?.flowElement?.name ?? id);
99
+ * });
100
+ * },
101
+ * };
102
+ * ```
103
+ */
104
+ export interface CanvasApi {
105
+ /** The host element passed to {@link CanvasOptions.container}. */
106
+ readonly container: HTMLElement;
107
+ /** The root `<svg>` element. */
108
+ readonly svg: SVGSVGElement;
109
+ /** The viewport `<g>` element. All diagram content lives inside this group. */
110
+ readonly viewportEl: SVGGElement;
111
+ /** Returns the current viewport state (pan + zoom). */
112
+ getViewport(): ViewportState;
113
+ /** Programmatically updates viewport. Missing fields are preserved. */
114
+ setViewport(state: Partial<ViewportState>): void;
115
+ /** Returns all currently rendered shapes. */
116
+ getShapes(): RenderedShape[];
117
+ /** Returns all currently rendered edges. */
118
+ getEdges(): RenderedEdge[];
119
+ /** Returns the current color theme. */
120
+ getTheme(): Theme;
121
+ /** Sets the color theme. Pass `"auto"` to follow the OS preference. */
122
+ setTheme(theme: Theme): void;
123
+ /**
124
+ * Subscribes to a canvas event. Returns an unsubscribe function.
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * const off = api.on("element:click", (id) => console.log(id));
129
+ * off(); // unsubscribe
130
+ * ```
131
+ */
132
+ on<K extends keyof CanvasEvents>(event: K, handler: CanvasEvents[K]): () => void;
133
+ /** Emits a canvas event. Intended for use by plugins and internal code. */
134
+ emit<K extends keyof CanvasEvents>(event: K, ...args: Parameters<CanvasEvents[K]>): void;
135
+ }
136
+ /**
137
+ * A plugin that extends `BpmnCanvas` without modifying the core.
138
+ *
139
+ * Plugins follow a simple install/uninstall lifecycle:
140
+ * 1. `install(api)` is called once when the plugin is registered.
141
+ * 2. `uninstall()` is called when the canvas is destroyed.
142
+ *
143
+ * Use `install` to hook into events, add overlays, or register keyboard shortcuts.
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * // Log all element clicks
148
+ * const logPlugin: CanvasPlugin = {
149
+ * name: "click-logger",
150
+ * install(api) {
151
+ * api.on("element:click", (id, e) => {
152
+ * console.log(`${id} clicked at (${e.clientX}, ${e.clientY})`);
153
+ * });
154
+ * },
155
+ * };
156
+ *
157
+ * const canvas = new BpmnCanvas({
158
+ * container: document.getElementById("app")!,
159
+ * plugins: [logPlugin],
160
+ * });
161
+ * ```
162
+ */
163
+ export interface CanvasPlugin {
164
+ /** A unique name that identifies this plugin. */
165
+ readonly name: string;
166
+ /**
167
+ * Called once when the plugin is installed. Hook into canvas events here.
168
+ * @param api — The canvas API handle.
169
+ */
170
+ install(api: CanvasApi): void;
171
+ /**
172
+ * Called when the canvas is destroyed.
173
+ * Clean up any resources your plugin allocated (DOM nodes, timers, etc.).
174
+ */
175
+ uninstall?(): void;
176
+ }
177
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,77 @@
1
+ import type { ViewportState } from "./types.js";
2
+ /**
3
+ * Controls the pan/zoom viewport of the canvas.
4
+ *
5
+ * Uses the Pointer Events API (covering mouse, touch, and pen) with
6
+ * `requestAnimationFrame`-batched rendering so interactions stay smooth at
7
+ * 60 fps with no perceptible input lag.
8
+ *
9
+ * - **Mouse**: left-button drag to pan, scroll wheel to zoom toward cursor.
10
+ * - **Touch**: one-finger drag to pan, two-finger pinch to zoom.
11
+ * - **Keyboard**: handled externally in `keyboard.ts` via `zoomAt` / `set`.
12
+ */
13
+ export declare class ViewportController {
14
+ /** The host div — receives the `is-panning` CSS class. */
15
+ private readonly _host;
16
+ /** The SVG element that receives pointer/wheel events. */
17
+ private readonly _svg;
18
+ /** The `<g>` element whose `transform` attribute is updated. */
19
+ private readonly _group;
20
+ /** The dot-grid `<pattern>` element (optional). Kept in sync with the viewport. */
21
+ private readonly _gridPattern;
22
+ /** Called on every rendered frame with the new viewport state. */
23
+ private readonly _onChanged;
24
+ private _tx;
25
+ private _ty;
26
+ private _scale;
27
+ private _raf;
28
+ private _dirty;
29
+ private _dragging;
30
+ private _lastX;
31
+ private _lastY;
32
+ private _dragDist;
33
+ private _locked;
34
+ private _activePointers;
35
+ private _lastPinchDist;
36
+ constructor(
37
+ /** The host div — receives the `is-panning` CSS class. */
38
+ _host: HTMLElement,
39
+ /** The SVG element that receives pointer/wheel events. */
40
+ _svg: SVGSVGElement,
41
+ /** The `<g>` element whose `transform` attribute is updated. */
42
+ _group: SVGGElement,
43
+ /** The dot-grid `<pattern>` element (optional). Kept in sync with the viewport. */
44
+ _gridPattern: SVGPatternElement | null,
45
+ /** Called on every rendered frame with the new viewport state. */
46
+ _onChanged: (state: ViewportState) => void);
47
+ /** Current viewport state snapshot. */
48
+ get state(): ViewportState;
49
+ /**
50
+ * When locked, pointer-down is ignored (no new pan starts) and pointer-move
51
+ * does not continue any in-progress pan. Scroll-wheel zoom is unaffected.
52
+ */
53
+ lock(locked: boolean): void;
54
+ /**
55
+ * Whether the last pointer interaction was a drag (as opposed to a click).
56
+ * The canvas uses this to suppress `element:click` events after panning.
57
+ */
58
+ get didPan(): boolean;
59
+ /** Programmatically sets viewport state. Any omitted fields are unchanged. */
60
+ set(state: Partial<ViewportState>): void;
61
+ /**
62
+ * Zooms the viewport by `factor`, keeping the point at (`screenX`, `screenY`)
63
+ * fixed in screen space (zoom-toward-cursor behaviour).
64
+ */
65
+ zoomAt(screenX: number, screenY: number, factor: number): void;
66
+ /** Removes all event listeners and cancels any pending animation frame. */
67
+ destroy(): void;
68
+ private _scheduleApply;
69
+ private _apply;
70
+ private readonly _onWheel;
71
+ private readonly _onPointerDown;
72
+ private readonly _onPointerMove;
73
+ private readonly _onPointerUp;
74
+ private _bindEvents;
75
+ private _unbindEvents;
76
+ }
77
+ //# sourceMappingURL=viewport.d.ts.map
@@ -0,0 +1,203 @@
1
+ const MIN_SCALE = 0.05;
2
+ const MAX_SCALE = 10;
3
+ function clamp(v, min, max) {
4
+ return Math.max(min, Math.min(max, v));
5
+ }
6
+ /**
7
+ * Controls the pan/zoom viewport of the canvas.
8
+ *
9
+ * Uses the Pointer Events API (covering mouse, touch, and pen) with
10
+ * `requestAnimationFrame`-batched rendering so interactions stay smooth at
11
+ * 60 fps with no perceptible input lag.
12
+ *
13
+ * - **Mouse**: left-button drag to pan, scroll wheel to zoom toward cursor.
14
+ * - **Touch**: one-finger drag to pan, two-finger pinch to zoom.
15
+ * - **Keyboard**: handled externally in `keyboard.ts` via `zoomAt` / `set`.
16
+ */
17
+ export class ViewportController {
18
+ _host;
19
+ _svg;
20
+ _group;
21
+ _gridPattern;
22
+ _onChanged;
23
+ _tx = 0;
24
+ _ty = 0;
25
+ _scale = 1;
26
+ _raf = null;
27
+ _dirty = false;
28
+ // Pan state
29
+ _dragging = false;
30
+ _lastX = 0;
31
+ _lastY = 0;
32
+ _dragDist = 0;
33
+ _locked = false;
34
+ // Pinch state
35
+ _activePointers = new Map();
36
+ _lastPinchDist = 0;
37
+ constructor(
38
+ /** The host div — receives the `is-panning` CSS class. */
39
+ _host,
40
+ /** The SVG element that receives pointer/wheel events. */
41
+ _svg,
42
+ /** The `<g>` element whose `transform` attribute is updated. */
43
+ _group,
44
+ /** The dot-grid `<pattern>` element (optional). Kept in sync with the viewport. */
45
+ _gridPattern,
46
+ /** Called on every rendered frame with the new viewport state. */
47
+ _onChanged) {
48
+ this._host = _host;
49
+ this._svg = _svg;
50
+ this._group = _group;
51
+ this._gridPattern = _gridPattern;
52
+ this._onChanged = _onChanged;
53
+ this._bindEvents();
54
+ }
55
+ /** Current viewport state snapshot. */
56
+ get state() {
57
+ return { tx: this._tx, ty: this._ty, scale: this._scale };
58
+ }
59
+ /**
60
+ * When locked, pointer-down is ignored (no new pan starts) and pointer-move
61
+ * does not continue any in-progress pan. Scroll-wheel zoom is unaffected.
62
+ */
63
+ lock(locked) {
64
+ this._locked = locked;
65
+ }
66
+ /**
67
+ * Whether the last pointer interaction was a drag (as opposed to a click).
68
+ * The canvas uses this to suppress `element:click` events after panning.
69
+ */
70
+ get didPan() {
71
+ return this._dragDist > 4;
72
+ }
73
+ /** Programmatically sets viewport state. Any omitted fields are unchanged. */
74
+ set(state) {
75
+ if (state.tx !== undefined)
76
+ this._tx = state.tx;
77
+ if (state.ty !== undefined)
78
+ this._ty = state.ty;
79
+ if (state.scale !== undefined)
80
+ this._scale = clamp(state.scale, MIN_SCALE, MAX_SCALE);
81
+ this._scheduleApply();
82
+ }
83
+ /**
84
+ * Zooms the viewport by `factor`, keeping the point at (`screenX`, `screenY`)
85
+ * fixed in screen space (zoom-toward-cursor behaviour).
86
+ */
87
+ zoomAt(screenX, screenY, factor) {
88
+ const newScale = clamp(this._scale * factor, MIN_SCALE, MAX_SCALE);
89
+ const ratio = newScale / this._scale;
90
+ this._tx = screenX - (screenX - this._tx) * ratio;
91
+ this._ty = screenY - (screenY - this._ty) * ratio;
92
+ this._scale = newScale;
93
+ this._scheduleApply();
94
+ }
95
+ /** Removes all event listeners and cancels any pending animation frame. */
96
+ destroy() {
97
+ this._unbindEvents();
98
+ if (this._raf !== null)
99
+ cancelAnimationFrame(this._raf);
100
+ }
101
+ // ── Private ──────────────────────────────────────────────────────
102
+ _scheduleApply() {
103
+ this._dirty = true;
104
+ if (this._raf === null) {
105
+ this._raf = requestAnimationFrame(() => {
106
+ this._raf = null;
107
+ if (this._dirty) {
108
+ this._dirty = false;
109
+ this._apply();
110
+ }
111
+ });
112
+ }
113
+ }
114
+ _apply() {
115
+ const t = `translate(${this._tx} ${this._ty}) scale(${this._scale})`;
116
+ this._group.setAttribute("transform", t);
117
+ if (this._gridPattern) {
118
+ this._gridPattern.setAttribute("patternTransform", t);
119
+ }
120
+ this._onChanged(this.state);
121
+ }
122
+ _onWheel = (e) => {
123
+ e.preventDefault();
124
+ const rect = this._svg.getBoundingClientRect();
125
+ const cx = e.clientX - rect.left;
126
+ const cy = e.clientY - rect.top;
127
+ // Normalise across deltaMode variants (pixels, lines, pages)
128
+ const delta = e.deltaMode === 1 ? e.deltaY * 24 : e.deltaMode === 2 ? e.deltaY * 400 : e.deltaY;
129
+ const factor = Math.exp(-delta * 0.001);
130
+ this.zoomAt(cx, cy, factor);
131
+ };
132
+ _onPointerDown = (e) => {
133
+ // Only pan with left mouse button (button=0) or touch/pen
134
+ if (e.pointerType === "mouse" && e.button !== 0)
135
+ return;
136
+ if (this._locked)
137
+ return;
138
+ this._activePointers.set(e.pointerId, e);
139
+ this._svg.setPointerCapture(e.pointerId);
140
+ this._dragDist = 0;
141
+ if (this._activePointers.size === 1) {
142
+ this._dragging = true;
143
+ this._lastX = e.clientX;
144
+ this._lastY = e.clientY;
145
+ this._host.classList.add("is-panning");
146
+ }
147
+ };
148
+ _onPointerMove = (e) => {
149
+ this._activePointers.set(e.pointerId, e);
150
+ if (this._locked)
151
+ return;
152
+ if (this._activePointers.size >= 2) {
153
+ // Two-finger pinch-to-zoom
154
+ const pts = [...this._activePointers.values()];
155
+ const a = pts[0];
156
+ const b = pts[1];
157
+ if (!a || !b)
158
+ return;
159
+ const dist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
160
+ if (this._lastPinchDist > 0) {
161
+ const cx = (a.clientX + b.clientX) / 2;
162
+ const cy = (a.clientY + b.clientY) / 2;
163
+ const rect = this._svg.getBoundingClientRect();
164
+ this.zoomAt(cx - rect.left, cy - rect.top, dist / this._lastPinchDist);
165
+ }
166
+ this._lastPinchDist = dist;
167
+ return;
168
+ }
169
+ if (!this._dragging)
170
+ return;
171
+ const dx = e.clientX - this._lastX;
172
+ const dy = e.clientY - this._lastY;
173
+ this._dragDist += Math.abs(dx) + Math.abs(dy);
174
+ this._lastX = e.clientX;
175
+ this._lastY = e.clientY;
176
+ this._tx += dx;
177
+ this._ty += dy;
178
+ this._scheduleApply();
179
+ };
180
+ _onPointerUp = (e) => {
181
+ this._activePointers.delete(e.pointerId);
182
+ this._lastPinchDist = 0;
183
+ if (this._activePointers.size === 0) {
184
+ this._dragging = false;
185
+ this._host.classList.remove("is-panning");
186
+ }
187
+ };
188
+ _bindEvents() {
189
+ this._svg.addEventListener("wheel", this._onWheel, { passive: false });
190
+ this._svg.addEventListener("pointerdown", this._onPointerDown);
191
+ this._svg.addEventListener("pointermove", this._onPointerMove);
192
+ this._svg.addEventListener("pointerup", this._onPointerUp);
193
+ this._svg.addEventListener("pointercancel", this._onPointerUp);
194
+ }
195
+ _unbindEvents() {
196
+ this._svg.removeEventListener("wheel", this._onWheel);
197
+ this._svg.removeEventListener("pointerdown", this._onPointerDown);
198
+ this._svg.removeEventListener("pointermove", this._onPointerMove);
199
+ this._svg.removeEventListener("pointerup", this._onPointerUp);
200
+ this._svg.removeEventListener("pointercancel", this._onPointerUp);
201
+ }
202
+ }
203
+ //# sourceMappingURL=viewport.js.map
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@bpmnkit/canvas",
3
+ "version": "0.0.8",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": ["dist/**/*.js", "dist/**/*.d.ts"],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "typecheck": "tsc --noEmit",
17
+ "check": "biome check .",
18
+ "test": "vitest run"
19
+ },
20
+ "dependencies": {
21
+ "@bpmnkit/core": "workspace:*"
22
+ },
23
+ "description": "Zero-dependency SVG BPMN viewer with pan/zoom, theming, and a plugin API",
24
+ "keywords": ["bpmn", "viewer", "svg", "canvas", "workflow", "typescript"],
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/bpmnkit/monorepo"
29
+ }
30
+ }