@visns-studio/visns-components 4.10.13 → 4.10.15

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,365 @@
1
+ import React, { PureComponent } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import History from './history';
4
+ import Select from './select';
5
+ import Pencil from './pencil';
6
+ import Line from './line';
7
+ import Arrow from './arrow';
8
+ import Rectangle from './rectangle';
9
+ import Circle from './circle';
10
+ import Pan from './pan';
11
+ import Tool from './tools';
12
+ import RectangleLabel from './rectangle-label';
13
+ import DefaultTool from './default-tool';
14
+ import * as fabric from 'fabric'; // v6
15
+ import { toast } from 'react-toastify'; // Import toast
16
+
17
+ // Debounce function
18
+ function debounce(func, delay) {
19
+ let timeout;
20
+ return (...args) => {
21
+ clearTimeout(timeout);
22
+ timeout = setTimeout(() => func.apply(this, args), delay);
23
+ };
24
+ }
25
+
26
+ class SketchField extends PureComponent {
27
+ static propTypes = {
28
+ lineColor: PropTypes.string,
29
+ lineWidth: PropTypes.number,
30
+ lineRadius: PropTypes.number,
31
+ fillColor: PropTypes.string,
32
+ backgroundColor: PropTypes.string,
33
+ backgroundImage: PropTypes.string,
34
+ opacity: PropTypes.number,
35
+ undoSteps: PropTypes.number,
36
+ tool: PropTypes.string,
37
+ imageFormat: PropTypes.string,
38
+ value: PropTypes.object,
39
+ forceValue: PropTypes.bool,
40
+ widthCorrection: PropTypes.number,
41
+ heightCorrection: PropTypes.number,
42
+ onChange: PropTypes.func,
43
+ defaultValue: PropTypes.object,
44
+ width: PropTypes.number,
45
+ height: PropTypes.number,
46
+ onObjectAdded: PropTypes.func,
47
+ onObjectModified: PropTypes.func,
48
+ onObjectRemoved: PropTypes.func,
49
+ className: PropTypes.string,
50
+ style: PropTypes.object,
51
+ };
52
+
53
+ static defaultProps = {
54
+ lineColor: 'black',
55
+ lineWidth: 10,
56
+ lineRadius: 10,
57
+ fillColor: 'transparent',
58
+ backgroundColor: 'transparent',
59
+ opacity: 1.0,
60
+ undoSteps: 25,
61
+ tool: null,
62
+ widthCorrection: 0,
63
+ heightCorrection: 0,
64
+ forceValue: false,
65
+ onObjectAdded: () => null,
66
+ onObjectModified: () => null,
67
+ onObjectRemoved: () => null,
68
+ };
69
+
70
+ _initTools = (fabricCanvas) => {
71
+ this._tools = {
72
+ [Tool.Select]: new Select(fabricCanvas),
73
+ [Tool.Pencil]: new Pencil(fabricCanvas),
74
+ [Tool.Line]: new Line(fabricCanvas),
75
+ [Tool.Arrow]: new Arrow(fabricCanvas),
76
+ [Tool.Rectangle]: new Rectangle(fabricCanvas),
77
+ [Tool.RectangleLabel]: new RectangleLabel(fabricCanvas),
78
+ [Tool.Circle]: new Circle(fabricCanvas),
79
+ [Tool.Pan]: new Pan(fabricCanvas),
80
+ [Tool.DefaultTool]: new DefaultTool(fabricCanvas),
81
+ };
82
+ };
83
+
84
+ setBackgroundImage = async (imageUrl) => {
85
+ try {
86
+ const img = await fabric.Image.fromURL(imageUrl, {
87
+ crossOrigin: 'anonymous', // Enable CORS
88
+ });
89
+
90
+ // Scale the image to fit the canvas dimensions
91
+ img.scaleToWidth(this._fc.width);
92
+ img.scaleToHeight(this._fc.height);
93
+ img.set({ selectable: false, evented: false });
94
+
95
+ // Set the image directly as the background
96
+ this._fc.backgroundImage = img;
97
+ this._fc.renderAll(); // Render the canvas immediately
98
+ } catch (error) {
99
+ console.error('Failed to load background image:', error);
100
+ }
101
+ };
102
+
103
+ saveCanvasState = () => {
104
+ // Return the current state of the canvas as JSON
105
+ const state = this._fc.toJSON();
106
+ const backgroundImage = this._fc.backgroundImage
107
+ ? {
108
+ src: this._fc.backgroundImage._element.src,
109
+ left: this._fc.backgroundImage.left,
110
+ top: this._fc.backgroundImage.top,
111
+ scaleX: this._fc.backgroundImage.scaleX,
112
+ scaleY: this._fc.backgroundImage.scaleY,
113
+ }
114
+ : null;
115
+
116
+ return {
117
+ state,
118
+ backgroundImage,
119
+ };
120
+ };
121
+
122
+ exportCanvasImage = async (format = 'png') => {
123
+ // Ensure all images are loaded with CORS
124
+ const imageObjects = this._fc.getObjects('image');
125
+ for (const imgObj of imageObjects) {
126
+ if (imgObj.crossOrigin !== 'anonymous') {
127
+ // Set crossOrigin for images that are not set
128
+ imgObj.set({ crossOrigin: 'anonymous' });
129
+ }
130
+ }
131
+
132
+ // Get the Base64 representation of the canvas image
133
+ const dataURL = this._fc.toDataURL({
134
+ format: format,
135
+ quality: 0.8,
136
+ });
137
+
138
+ // Return the Base64 string
139
+ return dataURL; // This returns a promise
140
+ };
141
+
142
+ clearCanvas = () => {
143
+ // Clear all objects from the canvas while keeping the background image
144
+ const backgroundImage = this._fc.backgroundImage;
145
+ this._fc.clear();
146
+
147
+ // Restore the background image after clearing
148
+ if (backgroundImage) {
149
+ this._fc.backgroundImage = backgroundImage;
150
+ }
151
+ this._fc.renderAll(); // Re-render to show the background image
152
+ };
153
+
154
+ _resize = (e) => {
155
+ const { widthCorrection, heightCorrection } = this.props;
156
+ const canvas = this._fc;
157
+
158
+ const { offsetWidth, clientHeight } = this._container;
159
+ const wfactor = (offsetWidth - widthCorrection) / canvas.width;
160
+ const hfactor = (clientHeight - heightCorrection) / canvas.height;
161
+
162
+ canvas.setWidth(offsetWidth - widthCorrection);
163
+ canvas.setHeight(clientHeight - heightCorrection);
164
+
165
+ canvas.getObjects().forEach((obj) => {
166
+ obj.scaleX *= wfactor;
167
+ obj.scaleY *= hfactor;
168
+ obj.left *= wfactor;
169
+ obj.top *= hfactor;
170
+ obj.setCoords();
171
+ });
172
+
173
+ this.setState({ parentWidth: offsetWidth });
174
+ canvas.requestRenderAll();
175
+ };
176
+
177
+ _backgroundColor = (color) => {
178
+ if (this._fc && color) {
179
+ this._fc.backgroundColor = color;
180
+ this._fc.requestRenderAll();
181
+ }
182
+ };
183
+
184
+ undo = () => {
185
+ if (this._history && this._history.canUndo()) {
186
+ const previousState = this._history.undo();
187
+ if (previousState) {
188
+ // Restore the previous state
189
+ this.fromJSON(previousState.state);
190
+ // Reapply the background image after undoing
191
+ if (previousState.backgroundImage) {
192
+ this.setBackgroundImage(previousState.backgroundImage.src);
193
+ }
194
+ } else {
195
+ this._fc.clear(); // If no previous state, clear the canvas
196
+ }
197
+ this._fc.renderAll(); // Re-render canvas
198
+ this.props.onChange && this.props.onChange();
199
+ }
200
+ };
201
+
202
+ redo = () => {
203
+ if (this._history && this._history.canRedo()) {
204
+ const nextState = this._history.redo();
205
+ if (nextState) {
206
+ this.fromJSON(nextState.state);
207
+ // Reapply the background image after redoing
208
+ if (nextState.backgroundImage) {
209
+ this.setBackgroundImage(nextState.backgroundImage.src);
210
+ }
211
+ this._fc.renderAll(); // Re-render canvas
212
+ this.props.onChange && this.props.onChange();
213
+ }
214
+ }
215
+ };
216
+
217
+ fromJSON = (json) => {
218
+ if (!json) return;
219
+ this._fc.loadFromJSON(json, () => {
220
+ this._fc.requestRenderAll();
221
+ this.props.onChange && this.props.onChange();
222
+ });
223
+ };
224
+
225
+ componentDidMount = () => {
226
+ const {
227
+ tool,
228
+ undoSteps,
229
+ backgroundColor,
230
+ width = 800,
231
+ height = 600,
232
+ backgroundImage,
233
+ defaultValue,
234
+ } = this.props;
235
+
236
+ this._fc = new fabric.Canvas(this._canvas);
237
+ this._fc.setWidth(width);
238
+ this._fc.setHeight(height);
239
+
240
+ this._initTools(this._fc);
241
+ this._backgroundColor(backgroundColor);
242
+
243
+ const selectedTool = this._tools[tool];
244
+ if (selectedTool) {
245
+ selectedTool.configureCanvas(this.props);
246
+ selectedTool.attachListeners();
247
+ }
248
+ this._selectedTool = selectedTool;
249
+
250
+ window.addEventListener('resize', this._resize, false);
251
+
252
+ this._history = new History(undoSteps);
253
+
254
+ this._fc.on('object:added', this._captureState);
255
+ this._fc.on('object:modified', this._captureState);
256
+ this._fc.on('object:removed', this._captureState);
257
+
258
+ this.setState({ canvasReady: true }, () => {
259
+ // Load the defaultValue to set the initial state of the canvas
260
+ if (defaultValue) {
261
+ // Show loading toast
262
+ setTimeout(() => {
263
+ this.fromJSON(defaultValue);
264
+ }, 1000);
265
+ } else {
266
+ if (backgroundImage) {
267
+ this.setBackgroundImage(backgroundImage);
268
+ }
269
+ }
270
+ });
271
+
272
+ // Load the defaultValue to set the initial state of the canvas
273
+ if (defaultValue) {
274
+ console.info('test');
275
+ // Show loading toast
276
+ const toastId = toast.loading('Loading Canvas...');
277
+ setTimeout(() => {
278
+ this.fromJSON(defaultValue);
279
+ // Update toast after loading is done
280
+ toast.update(toastId, {
281
+ render: 'Saved Canvas loaded!',
282
+ type: 'success',
283
+ isLoading: false,
284
+ autoClose: 2000,
285
+ });
286
+ }, 1000);
287
+ } else {
288
+ if (backgroundImage) {
289
+ this.setBackgroundImage(backgroundImage);
290
+ }
291
+ }
292
+ };
293
+
294
+ _captureState = debounce(() => {
295
+ if (this._history && this._fc) {
296
+ const state = this._fc.toJSON();
297
+ const backgroundImage = this._fc.backgroundImage
298
+ ? {
299
+ src: this._fc.backgroundImage._element.src,
300
+ left: this._fc.backgroundImage.left,
301
+ top: this._fc.backgroundImage.top,
302
+ scaleX: this._fc.backgroundImage.scaleX,
303
+ scaleY: this._fc.backgroundImage.scaleY,
304
+ }
305
+ : null;
306
+
307
+ this._history.keep({
308
+ state,
309
+ backgroundImage,
310
+ });
311
+ }
312
+ }, 300);
313
+
314
+ componentWillUnmount = () => {
315
+ window.removeEventListener('resize', this._resize);
316
+ };
317
+
318
+ componentDidUpdate = (prevProps) => {
319
+ if (this.props.tool !== prevProps.tool) {
320
+ if (this._selectedTool) {
321
+ this._selectedTool.detachListeners();
322
+ }
323
+
324
+ this._selectedTool = this._tools[this.props.tool];
325
+ this._selectedTool.configureCanvas(this.props);
326
+ this._selectedTool.attachListeners();
327
+ }
328
+
329
+ if (this.props.backgroundColor !== prevProps.backgroundColor) {
330
+ this._backgroundColor(this.props.backgroundColor);
331
+ }
332
+
333
+ if (this.props.defaultValue !== prevProps.defaultValue) {
334
+ this.fromJSON(this.props.defaultValue);
335
+ } else {
336
+ if (this.props.backgroundImage !== prevProps.backgroundImage) {
337
+ // console.log(
338
+ // 'Setting new background image:',
339
+ // this.props.backgroundImage
340
+ // );
341
+ this.setBackgroundImage(this.props.backgroundImage);
342
+ }
343
+ }
344
+ };
345
+
346
+ render() {
347
+ const { className, style, width = 800, height = 600 } = this.props;
348
+ const canvasDivStyle = { ...style, width, height };
349
+
350
+ return (
351
+ <div
352
+ className={className}
353
+ ref={(c) => (this._container = c)}
354
+ style={canvasDivStyle}
355
+ >
356
+ <canvas ref={(c) => (this._canvas = c)}>
357
+ Sorry, Canvas HTML5 element is not supported by your
358
+ browser.
359
+ </canvas>
360
+ </div>
361
+ );
362
+ }
363
+ }
364
+
365
+ export default SketchField;
@@ -0,0 +1,108 @@
1
+ import FabricCanvasTool from './fabrictool';
2
+ import {
3
+ Line as FabricLine,
4
+ Triangle as FabricTriangle,
5
+ Group as FabricGroup,
6
+ } from 'fabric'; // Import specific classes
7
+
8
+ class Arrow extends FabricCanvasTool {
9
+ configureCanvas(props) {
10
+ const canvas = this._canvas;
11
+
12
+ // Disable selection and set up drawing mode
13
+ canvas.isDrawingMode = false;
14
+ canvas.selection = false;
15
+ canvas.forEachObject((o) => {
16
+ o.selectable = false;
17
+ o.evented = false;
18
+ });
19
+
20
+ // Set arrow line properties
21
+ this._width = props.lineWidth || 1;
22
+ this._color = props.lineColor || 'black';
23
+ }
24
+
25
+ doMouseDown(o) {
26
+ this.isDown = true;
27
+ const canvas = this._canvas;
28
+
29
+ // Get initial mouse pointer position
30
+ const pointer = canvas.getPointer(o.e);
31
+ const points = [pointer.x, pointer.y, pointer.x, pointer.y];
32
+
33
+ // Create the main line of the arrow
34
+ this.line = new FabricLine(points, {
35
+ strokeWidth: this._width,
36
+ fill: this._color,
37
+ stroke: this._color,
38
+ originX: 'center',
39
+ originY: 'center',
40
+ selectable: false,
41
+ evented: false,
42
+ });
43
+
44
+ // Create the arrowhead as a triangle
45
+ this.head = new FabricTriangle({
46
+ fill: this._color,
47
+ left: pointer.x,
48
+ top: pointer.y,
49
+ originX: 'center',
50
+ originY: 'center',
51
+ height: 3 * this._width,
52
+ width: 3 * this._width,
53
+ selectable: false,
54
+ evented: false,
55
+ angle: 90,
56
+ });
57
+
58
+ // Add both line and head to the canvas
59
+ canvas.add(this.line);
60
+ canvas.add(this.head);
61
+ }
62
+
63
+ doMouseMove(o) {
64
+ if (!this.isDown) return;
65
+ const canvas = this._canvas;
66
+ const pointer = canvas.getPointer(o.e);
67
+
68
+ // Update the line endpoint as mouse moves
69
+ this.line.set({ x2: pointer.x, y2: pointer.y });
70
+ this.line.setCoords();
71
+
72
+ // Calculate angle for the arrowhead to point along the line
73
+ const xDelta = pointer.x - this.line.x1;
74
+ const yDelta = pointer.y - this.line.y1;
75
+ this.head.set({
76
+ left: pointer.x,
77
+ top: pointer.y,
78
+ angle: 90 + (Math.atan2(yDelta, xDelta) * 180) / Math.PI,
79
+ });
80
+
81
+ // Render changes
82
+ canvas.requestRenderAll();
83
+ }
84
+
85
+ doMouseUp() {
86
+ this.isDown = false;
87
+ const canvas = this._canvas;
88
+
89
+ // Remove separate line and head and group them into an arrow
90
+ canvas.remove(this.line);
91
+ canvas.remove(this.head);
92
+
93
+ const arrow = new FabricGroup([this.line, this.head], {
94
+ selectable: true,
95
+ evented: true,
96
+ });
97
+
98
+ // Add the grouped arrow back to the canvas
99
+ canvas.add(arrow);
100
+ canvas.requestRenderAll();
101
+ }
102
+
103
+ doMouseOut() {
104
+ this.isDown = false;
105
+ }
106
+ }
107
+
108
+ export default Arrow;
@@ -0,0 +1,75 @@
1
+ import FabricCanvasTool from './fabrictool';
2
+ import { Circle as FabricCircle } from 'fabric';
3
+
4
+ class Circle extends FabricCanvasTool {
5
+ constructor(canvas) {
6
+ super(canvas);
7
+ // Define event handlers as arrow functions for consistent references
8
+ this.doMouseDown = (o) => this._doMouseDown(o);
9
+ this.doMouseMove = (o) => this._doMouseMove(o);
10
+ this.doMouseUp = () => this._doMouseUp();
11
+ }
12
+
13
+ configureCanvas(props) {
14
+ const canvas = this._canvas;
15
+ canvas.isDrawingMode = false; // Disable drawing mode for custom drawing
16
+ canvas.selection = false;
17
+ this._width = props.lineWidth || 2;
18
+ this._color = props.lineColor || 'black';
19
+ this._fill = props.fillColor || 'transparent';
20
+ }
21
+
22
+ attachListeners() {
23
+ this._canvas.on('mouse:down', this.doMouseDown);
24
+ this._canvas.on('mouse:move', this.doMouseMove);
25
+ this._canvas.on('mouse:up', this.doMouseUp);
26
+ }
27
+
28
+ detachListeners() {
29
+ this._canvas.off('mouse:down', this.doMouseDown);
30
+ this._canvas.off('mouse:move', this.doMouseMove);
31
+ this._canvas.off('mouse:up', this.doMouseUp);
32
+ }
33
+
34
+ _doMouseDown(o) {
35
+ const canvas = this._canvas;
36
+ this.isDown = true;
37
+ const pointer = canvas.getPointer(o.e);
38
+ this.startX = pointer.x;
39
+ this.startY = pointer.y;
40
+
41
+ this.circle = new FabricCircle({
42
+ left: this.startX,
43
+ top: this.startY,
44
+ originX: 'center',
45
+ originY: 'center',
46
+ strokeWidth: this._width,
47
+ stroke: this._color,
48
+ fill: this._fill,
49
+ radius: 1, // Start with a minimal radius, updated on `doMouseMove`
50
+ selectable: false,
51
+ evented: false,
52
+ });
53
+ canvas.add(this.circle);
54
+ }
55
+
56
+ _doMouseMove(o) {
57
+ if (!this.isDown) return;
58
+ const canvas = this._canvas;
59
+ const pointer = canvas.getPointer(o.e);
60
+ const radius = Math.sqrt(
61
+ Math.pow(pointer.x - this.startX, 2) +
62
+ Math.pow(pointer.y - this.startY, 2)
63
+ );
64
+
65
+ this.circle.set({ radius });
66
+ this.circle.setCoords();
67
+ canvas.renderAll();
68
+ }
69
+
70
+ _doMouseUp() {
71
+ this.isDown = false;
72
+ }
73
+ }
74
+
75
+ export default Circle;
@@ -0,0 +1,16 @@
1
+ /*eslint no-unused-vars: 0*/
2
+
3
+ import FabricCanvasTool from './fabrictool';
4
+
5
+ class DefaultTool extends FabricCanvasTool {
6
+ configureCanvas(props) {
7
+ let canvas = this._canvas;
8
+ canvas.isDrawingMode = canvas.selection = false;
9
+ canvas.forEachObject((o) => (o.selectable = o.evented = false));
10
+ canvas.discardActiveObject();
11
+ canvas.defaultCursor = 'pointer';
12
+ canvas.renderAll();
13
+ }
14
+ }
15
+
16
+ export default DefaultTool;
@@ -0,0 +1,22 @@
1
+ /* eslint no-unused-vars: 0 */
2
+
3
+ /**
4
+ * "Abstract" like base class for a Canvas tool
5
+ */
6
+ class FabricCanvasTool {
7
+ constructor(canvas) {
8
+ this._canvas = canvas;
9
+ }
10
+
11
+ configureCanvas(props) {}
12
+
13
+ doMouseUp(event) {}
14
+
15
+ doMouseDown(event) {}
16
+
17
+ doMouseMove(event) {}
18
+
19
+ doMouseOut(event) {}
20
+ }
21
+
22
+ export default FabricCanvasTool;