@bwp-web/canvas 0.15.0 → 1.1.0

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.
package/README.md ADDED
@@ -0,0 +1,307 @@
1
+ # @bwp-web/canvas
2
+
3
+ Interactive canvas editor and viewer for Biamp Workplace applications. Built on Fabric.js with React hooks, it provides shape creation, selection, pan/zoom, alignment guides, serialization, and DOM overlays out of the box.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bwp-web/canvas
9
+ ```
10
+
11
+ ### Peer Dependencies
12
+
13
+ - `react` >= 18.0.0
14
+ - `react-dom` >= 18.0.0
15
+ - `@mui/material` >= 7.0.0
16
+ - `@bwp-web/styles` >= 1.0.1
17
+ - `fabric` >= 7.2.0
18
+
19
+ ## Quick Start
20
+
21
+ ### Edit Canvas
22
+
23
+ Full editing with shape creation, selection, pan/zoom, alignment, and serialization:
24
+
25
+ ```tsx
26
+ import {
27
+ Canvas,
28
+ useEditCanvas,
29
+ enableDragToCreate,
30
+ createRectangle,
31
+ } from '@bwp-web/canvas';
32
+
33
+ function Editor() {
34
+ const canvas = useEditCanvas();
35
+
36
+ const startDragMode = () => {
37
+ canvas.setMode((c, viewport) =>
38
+ enableDragToCreate(
39
+ c,
40
+ (c, bounds) =>
41
+ createRectangle(c, {
42
+ left: bounds.startX + bounds.width / 2,
43
+ top: bounds.startY + bounds.height / 2,
44
+ width: bounds.width,
45
+ height: bounds.height,
46
+ }),
47
+ { onCreated: () => canvas.setMode(null), viewport },
48
+ ),
49
+ );
50
+ };
51
+
52
+ return (
53
+ <div>
54
+ <button onClick={startDragMode}>Draw Rectangle</button>
55
+ <Canvas onReady={canvas.onReady} width={800} height={600} />
56
+ </div>
57
+ );
58
+ }
59
+ ```
60
+
61
+ ### View Canvas
62
+
63
+ Read-only display with pan/zoom and object styling:
64
+
65
+ ```tsx
66
+ import { Canvas, useViewCanvas, loadCanvas } from '@bwp-web/canvas';
67
+
68
+ function Viewer({ savedJson }: { savedJson: object }) {
69
+ const canvas = useViewCanvas({
70
+ onReady: (c) => loadCanvas(c, savedJson),
71
+ });
72
+
73
+ return <Canvas onReady={canvas.onReady} width={800} height={600} />;
74
+ }
75
+ ```
76
+
77
+ ## Hooks
78
+
79
+ ### `useEditCanvas(options?)`
80
+
81
+ Full-featured editing hook with shape creation, selection, pan/zoom, alignment, vertex editing, keyboard shortcuts, and serialization.
82
+
83
+ ```tsx
84
+ const canvas = useEditCanvas({
85
+ canvasData: savedJson, // auto-load canvas data
86
+ filter: (obj) => ids.includes(obj.data?.id),
87
+ invertBackground: isDarkMode, // reactive background inversion
88
+ enableAlignment: true, // object alignment guides
89
+ scaledStrokes: true, // zoom-independent stroke widths
90
+ keyboardShortcuts: true, // Delete/Backspace to remove selected
91
+ vertexEdit: true, // double-click polygon to edit vertices
92
+ panAndZoom: true, // scroll to zoom, Cmd/Ctrl+drag to pan
93
+ rotationSnap: { interval: 15 }, // Shift+rotate snaps to 15°
94
+ autoFitToBackground: true, // auto-fit viewport to background image
95
+ backgroundResize: true, // auto-downscale large uploaded images
96
+ trackChanges: true, // expose isDirty / resetDirty
97
+ history: true, // undo/redo support
98
+ onReady: (canvas) => {}, // called after canvasData load + features init
99
+ });
100
+ ```
101
+
102
+ #### Return Value
103
+
104
+ | Property | Type | Description |
105
+ | ---------------------- | ------------------------------ | ---------------------------------------------------------- |
106
+ | `onReady` | `(canvas) => void` | Pass to `<Canvas onReady={...}>` |
107
+ | `canvasRef` | `RefObject<FabricCanvas>` | Direct access to the Fabric canvas |
108
+ | `zoom` | `number` | Current zoom level (reactive) |
109
+ | `objects` | `FabricObject[]` | Canvas objects (reactive, kept in sync) |
110
+ | `isLoading` | `boolean` | Whether canvas data is currently being loaded |
111
+ | `selected` | `FabricObject[]` | Currently selected objects (reactive) |
112
+ | `setMode` | `(setup \| null) => void` | Activate or deactivate an interaction mode |
113
+ | `setBackground` | `(url, opts?) => Promise<...>` | Load a background image |
114
+ | `isDirty` | `boolean` | Whether canvas has been modified since last `resetDirty()` |
115
+ | `resetDirty` | `() => void` | Reset the dirty flag after a successful save |
116
+ | `markDirty` | `() => void` | Manually mark the canvas as dirty |
117
+ | `undo` | `() => Promise<void>` | Undo last change (requires `history: true`) |
118
+ | `redo` | `() => Promise<void>` | Redo previously undone change (requires `history: true`) |
119
+ | `canUndo` | `boolean` | Whether undo is available (reactive) |
120
+ | `canRedo` | `boolean` | Whether redo is available (reactive) |
121
+ | `viewport.zoomIn` | `(step?) => void` | Zoom in toward center |
122
+ | `viewport.zoomOut` | `(step?) => void` | Zoom out from center |
123
+ | `viewport.reset` | `() => void` | Reset viewport |
124
+ | `viewport.panToObject` | `(object, options?) => void` | Pan viewport to center on an object |
125
+ | `viewport.zoomToFit` | `(object, options?) => void` | Zoom and pan to fit a specific object |
126
+
127
+ ### `useViewCanvas(options?)`
128
+
129
+ Read-only hook. Objects cannot be selected, created, or edited. The canvas is always in pan mode.
130
+
131
+ ```tsx
132
+ const canvas = useViewCanvas({
133
+ canvasData: savedJson,
134
+ filter: (obj) => objectIds.includes(obj.data?.id),
135
+ invertBackground: isDarkMode,
136
+ });
137
+
138
+ // Style objects dynamically
139
+ canvas.setObjectStyle('room-42', { fill: '#ff0000' });
140
+ canvas.setObjectStyles({
141
+ 'room-42': { fill: '#ff0000' },
142
+ 'room-43': { opacity: 0.5 },
143
+ });
144
+ canvas.setObjectStyleByType('DESK', { fill: '#cccccc' });
145
+ ```
146
+
147
+ ## Context Providers
148
+
149
+ When multiple sibling components need access to canvas state, use context providers instead of hooks directly. They expose the full API via React context with no prop drilling.
150
+
151
+ ```tsx
152
+ import {
153
+ EditCanvasProvider,
154
+ useEditCanvasContext,
155
+ useEditCanvasState,
156
+ useEditCanvasViewport,
157
+ Canvas,
158
+ } from '@bwp-web/canvas';
159
+
160
+ function App() {
161
+ return (
162
+ <EditCanvasProvider options={{ canvasData: savedJson, history: true }}>
163
+ <MyCanvas />
164
+ <SaveButton />
165
+ <ZoomDisplay />
166
+ </EditCanvasProvider>
167
+ );
168
+ }
169
+
170
+ function MyCanvas() {
171
+ const { onReady } = useEditCanvasContext();
172
+ return <Canvas onReady={onReady} />;
173
+ }
174
+
175
+ function SaveButton() {
176
+ // Does NOT re-render on zoom/scroll
177
+ const { isDirty, resetDirty } = useEditCanvasState();
178
+ return (
179
+ <button disabled={!isDirty} onClick={resetDirty}>
180
+ Save
181
+ </button>
182
+ );
183
+ }
184
+
185
+ function ZoomDisplay() {
186
+ // Does NOT re-render on selection/dirty changes
187
+ const { zoom } = useEditCanvasViewport();
188
+ return <span>{Math.round(zoom * 100)}%</span>;
189
+ }
190
+ ```
191
+
192
+ `ViewCanvasProvider` / `useViewCanvasContext` follow the same pattern for view-mode canvases.
193
+
194
+ ## `<Canvas>` Component
195
+
196
+ A thin React wrapper around a Fabric.js canvas. By default it fills its parent container and resizes automatically.
197
+
198
+ ```tsx
199
+ {
200
+ /* Auto-fill mode (default) */
201
+ }
202
+ <div style={{ width: 800, height: 600 }}>
203
+ <Canvas onReady={canvas.onReady} />
204
+ </div>;
205
+
206
+ {
207
+ /* Fixed-size mode */
208
+ }
209
+ <Canvas onReady={canvas.onReady} width={800} height={600} />;
210
+ ```
211
+
212
+ ## Shapes
213
+
214
+ ```tsx
215
+ import { createRectangle, createCircle, createPolygon } from '@bwp-web/canvas';
216
+
217
+ createRectangle(canvas, { left: 100, top: 100, width: 200, height: 150 });
218
+ createCircle(canvas, { left: 200, top: 200, radius: 50 });
219
+ createPolygon(canvas, {
220
+ points: [
221
+ { x: 0, y: 0 },
222
+ { x: 100, y: 0 },
223
+ { x: 50, y: 100 },
224
+ ],
225
+ });
226
+ ```
227
+
228
+ ## Interaction Modes
229
+
230
+ ```tsx
231
+ import {
232
+ enableClickToCreate,
233
+ enableDragToCreate,
234
+ enableDrawToCreate,
235
+ enableVertexEdit,
236
+ } from '@bwp-web/canvas';
237
+
238
+ // Single click to create a shape
239
+ canvas.setMode((c) => enableClickToCreate(c, factory, options));
240
+
241
+ // Drag to define bounds
242
+ canvas.setMode((c, viewport) => enableDragToCreate(c, factory, { viewport }));
243
+
244
+ // Vertex-by-vertex polygon drawing (click to add points, double-click to finish)
245
+ canvas.setMode((c, viewport) => enableDrawToCreate(c, factory, { viewport }));
246
+
247
+ // Edit polygon vertices (double-click a polygon to activate)
248
+ canvas.setMode((c) => enableVertexEdit(c, options));
249
+ ```
250
+
251
+ ## Serialization
252
+
253
+ ```tsx
254
+ import { serializeCanvas, loadCanvas } from '@bwp-web/canvas';
255
+
256
+ // Save
257
+ const json = serializeCanvas(canvas);
258
+
259
+ // Load
260
+ await loadCanvas(canvas, json);
261
+
262
+ // Load with object filter
263
+ await loadCanvas(canvas, json, { filter: (obj) => obj.data?.type === 'DESK' });
264
+ ```
265
+
266
+ ## Utility Hooks
267
+
268
+ | Hook | Description |
269
+ | ----------------------------------------------- | --------------------------------------------------------------------------------- |
270
+ | `useCanvasEvents(events)` | Subscribe to Fabric canvas events with automatic cleanup |
271
+ | `useCanvasTooltip({ getContent })` | Track hover over canvas objects, returns `{ visible, content, position, ref }` |
272
+ | `useCanvasClick(onClick, options?)` | Distinguish clicks from pan gestures; fires only on genuine clicks |
273
+ | `useObjectOverlay(canvasRef, object, options?)` | Position a DOM element over a Fabric object, kept in sync with pan/zoom/transform |
274
+
275
+ When used inside a provider, all utility hooks read `canvasRef` from context automatically — no need to pass it explicitly.
276
+
277
+ ## API Reference
278
+
279
+ | Module | Contents |
280
+ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
281
+ | Hooks | `useEditCanvas`, `useViewCanvas`, `Canvas`, `useCanvasEvents`, `useCanvasTooltip`, `useCanvasClick` |
282
+ | Context | `EditCanvasProvider`, `ViewCanvasProvider`, `useEditCanvasContext`, `useViewCanvasContext` |
283
+ | Shapes | `createRectangle`, `createCircle`, `createPolygon` and point/drag variants |
284
+ | Interactions | `enableClickToCreate`, `enableDragToCreate`, `enableDrawToCreate`, `enableVertexEdit` |
285
+ | Viewport | `enablePanAndZoom`, `resetViewport`, `ViewportController` |
286
+ | Alignment | `enableObjectAlignment`, `snapCursorPoint`, `enableRotationSnap` |
287
+ | Serialization | `serializeCanvas`, `loadCanvas`, `enableScaledStrokes` |
288
+ | Background | `setBackgroundImage`, `fitViewportToBackground`, `getBackgroundSrc`, `setBackgroundContrast`, `setBackgroundInverted`, `resizeImageUrl` |
289
+ | Keyboard | `enableKeyboardShortcuts`, `deleteObjects` |
290
+ | Overlay | `OverlayContainer`, `ObjectOverlay`, `OverlayContent`, `FixedSizeContent`, `OverlayBadge` |
291
+
292
+ ## Full Documentation
293
+
294
+ Detailed reference docs are available in the repository's [`/docs/canvas`](../../docs/canvas) folder (GitHub links):
295
+
296
+ | Document | Contents |
297
+ | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
298
+ | [hooks.md](../../docs/canvas/hooks.md) | `useEditCanvas`, `useViewCanvas`, context providers, utility hooks — full options and return value tables |
299
+ | [shapes.md](../../docs/canvas/shapes.md) | `createRectangle`, `createCircle`, `createPolygon` and all point/drag variants |
300
+ | [interactions.md](../../docs/canvas/interactions.md) | `enableClickToCreate`, `enableDragToCreate`, `enableDrawToCreate`, `enableVertexEdit` — all options |
301
+ | [viewport.md](../../docs/canvas/viewport.md) | `enablePanAndZoom`, `resetViewport`, `ViewportController` — all methods and options |
302
+ | [alignment.md](../../docs/canvas/alignment.md) | Object alignment guides, cursor snapping, rotation snapping, snap point extractors |
303
+ | [serialization.md](../../docs/canvas/serialization.md) | `serializeCanvas`, `loadCanvas`, scaled strokes, scaled border radius |
304
+ | [background.md](../../docs/canvas/background.md) | `setBackgroundImage`, contrast, invert, resize — all options |
305
+ | [keyboard.md](../../docs/canvas/keyboard.md) | `enableKeyboardShortcuts`, `deleteObjects` |
306
+ | [styles.md](../../docs/canvas/styles.md) | Default style objects, configuration constants, Fabric type augmentation |
307
+ | [overlay.md](../../docs/canvas/overlay.md) | `OverlayContainer`, `ObjectOverlay`, `OverlayContent`, `FixedSizeContent`, `OverlayBadge` — full API |
@@ -14,6 +14,12 @@ export interface CanvasTooltipState<T> {
14
14
  x: number;
15
15
  y: number;
16
16
  };
17
+ /**
18
+ * Ref to attach to the tooltip container element. When attached, position
19
+ * updates during pan/zoom are applied directly to the DOM — no React
20
+ * re-renders. The element should use `position: absolute`.
21
+ */
22
+ ref: RefObject<HTMLDivElement | null>;
17
23
  }
18
24
  /**
19
25
  * Track mouse hover over canvas objects and return tooltip state, using the
@@ -36,6 +42,9 @@ export declare function useCanvasTooltip<T>(options: UseCanvasTooltipOptions<T>)
36
42
  * coordinates relative to the canvas container element — suitable for absolute
37
43
  * positioning of a tooltip component.
38
44
  *
45
+ * Attach {@link CanvasTooltipState.ref | tooltip.ref} to the tooltip container
46
+ * for smooth, re-render-free position updates during pan and zoom.
47
+ *
39
48
  * @example
40
49
  * ```tsx
41
50
  * const tooltip = useCanvasTooltip(view.canvasRef, {
@@ -46,7 +55,10 @@ export declare function useCanvasTooltip<T>(options: UseCanvasTooltipOptions<T>)
46
55
  * <>
47
56
  * <Canvas onReady={view.onReady} />
48
57
  * {tooltip.visible && (
49
- * <div style={{ position: 'absolute', left: tooltip.position.x, top: tooltip.position.y }}>
58
+ * <div
59
+ * ref={tooltip.ref}
60
+ * style={{ position: 'absolute', left: tooltip.position.x, top: tooltip.position.y }}
61
+ * >
50
62
  * {tooltip.content?.id}
51
63
  * </div>
52
64
  * )}
@@ -1 +1 @@
1
- {"version":3,"file":"useCanvasTooltip.d.ts","sourceRoot":"","sources":["../../src/hooks/useCanvasTooltip.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,KAAK,EACV,MAAM,IAAI,YAAY,EAEtB,YAAY,EACb,MAAM,QAAQ,CAAC;AAGhB,MAAM,WAAW,uBAAuB,CAAC,CAAC;IACxC,wFAAwF;IACxF,UAAU,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,CAAC,GAAG,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,4EAA4E;IAC5E,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC;IAClB,gFAAgF;IAChF,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACpC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,OAAO,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAClC,kBAAkB,CAAC,CAAC,CAAC,CAAC;AACzB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,SAAS,EAAE,SAAS,CAAC,YAAY,GAAG,IAAI,CAAC,EACzC,OAAO,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAClC,kBAAkB,CAAC,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"useCanvasTooltip.d.ts","sourceRoot":"","sources":["../../src/hooks/useCanvasTooltip.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,KAAK,EACV,MAAM,IAAI,YAAY,EAEtB,YAAY,EACb,MAAM,QAAQ,CAAC;AAGhB,MAAM,WAAW,uBAAuB,CAAC,CAAC;IACxC,wFAAwF;IACxF,UAAU,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,CAAC,GAAG,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,4EAA4E;IAC5E,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC;IAClB,gFAAgF;IAChF,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC;;;;OAIG;IACH,GAAG,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACvC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,OAAO,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAClC,kBAAkB,CAAC,CAAC,CAAC,CAAC;AACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,SAAS,EAAE,SAAS,CAAC,YAAY,GAAG,IAAI,CAAC,EACzC,OAAO,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAClC,kBAAkB,CAAC,CAAC,CAAC,CAAC"}
@@ -139,9 +139,9 @@ export declare function useEditCanvas(options?: UseEditCanvasOptions): {
139
139
  /** Zoom out from the canvas center. Default step: 0.2. */
140
140
  zoomOut: (step?: number) => void;
141
141
  /** Pan the viewport to center on a specific object. */
142
- panToObject: (object: FabricObject, panOpts?: import("../viewport").PanToObjectOptions) => void;
142
+ panToObject: (object: FabricObject, panOpts?: import("..").PanToObjectOptions) => void;
143
143
  /** Zoom and pan to fit a specific object in the viewport. */
144
- zoomToFit: (object: FabricObject, fitOpts?: import("../viewport").ZoomToFitOptions) => void;
144
+ zoomToFit: (object: FabricObject, fitOpts?: import("..").ZoomToFitOptions) => void;
145
145
  };
146
146
  /** Whether vertex edit mode is currently active (reactive). */
147
147
  isEditingVertices: boolean;
@@ -84,9 +84,9 @@ export declare function useViewCanvas(options?: UseViewCanvasOptions): {
84
84
  /** Zoom out from the canvas center. Default step: 0.2. */
85
85
  zoomOut: (step?: number) => void;
86
86
  /** Pan the viewport to center on a specific object. */
87
- panToObject: (object: FabricObject, panOpts?: import("../viewport").PanToObjectOptions) => void;
87
+ panToObject: (object: FabricObject, panOpts?: import("..").PanToObjectOptions) => void;
88
88
  /** Zoom and pan to fit a specific object in the viewport. */
89
- zoomToFit: (object: FabricObject, fitOpts?: import("../viewport").ZoomToFitOptions) => void;
89
+ zoomToFit: (object: FabricObject, fitOpts?: import("..").ZoomToFitOptions) => void;
90
90
  };
91
91
  /** Update a single object's visual style by its `data.id`. */
92
92
  setObjectStyle: (id: string, style: ViewObjectStyle) => void;