-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcanvas.ts
422 lines (360 loc) · 11.2 KB
/
canvas.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
import { fabric } from "fabric";
import { v4 as uuid4 } from "uuid";
import {
CanvasMouseDown,
CanvasMouseMove,
CanvasMouseUp,
CanvasObjectModified,
CanvasObjectScaling,
CanvasPathCreated,
CanvasSelectionCreated,
RenderCanvas,
} from "@/types/type";
import { defaultNavElement } from "@/constants";
import { createSpecificShape } from "./shapes";
// initialize fabric canvas
export const initializeFabric = ({
fabricRef,
canvasRef,
}: {
fabricRef: React.MutableRefObject<fabric.Canvas | null>;
canvasRef: React.MutableRefObject<HTMLCanvasElement | null>;
}) => {
// get canvas element
const canvasElement = document.getElementById("canvas");
// create fabric canvas
const canvas = new fabric.Canvas(canvasRef.current, {
width: canvasElement?.clientWidth,
height: canvasElement?.clientHeight,
});
// set canvas reference to fabricRef so we can use it later anywhere outside canvas listener
fabricRef.current = canvas;
return canvas;
};
// instantiate creation of custom fabric object/shape and add it to canvas
export const handleCanvasMouseDown = ({
options,
canvas,
selectedShapeRef,
isDrawing,
shapeRef,
}: CanvasMouseDown) => {
// get pointer coordinates
const pointer = canvas.getPointer(options.e);
/**
* get target object i.e., the object that is clicked
* findtarget() returns the object that is clicked
*
* findTarget: http://fabricjs.com/docs/fabric.Canvas.html#findTarget
*/
const target = canvas.findTarget(options.e, false);
// set canvas drawing mode to false
canvas.isDrawingMode = false;
// if selected shape is freeform, set drawing mode to true and return
if (selectedShapeRef.current === "freeform") {
isDrawing.current = true;
canvas.isDrawingMode = true;
canvas.freeDrawingBrush.width = 5;
return;
}
canvas.isDrawingMode = false;
// if target is the selected shape or active selection, set isDrawing to false
if (
target &&
(target.type === selectedShapeRef.current ||
target.type === "activeSelection")
) {
isDrawing.current = false;
// set active object to target
canvas.setActiveObject(target);
/**
* setCoords() is used to update the controls of the object
* setCoords: http://fabricjs.com/docs/fabric.Object.html#setCoords
*/
target.setCoords();
} else {
isDrawing.current = true;
// create custom fabric object/shape and set it to shapeRef
shapeRef.current = createSpecificShape(
selectedShapeRef.current,
pointer as any
);
// if shapeRef is not null, add it to canvas
if (shapeRef.current) {
// add: http://fabricjs.com/docs/fabric.Canvas.html#add
canvas.add(shapeRef.current);
}
}
};
// handle mouse move event on canvas to draw shapes with different dimensions
export const handleCanvaseMouseMove = ({
options,
canvas,
isDrawing,
selectedShapeRef,
shapeRef,
syncShapeInStorage,
}: CanvasMouseMove) => {
// if selected shape is freeform, return
if (!isDrawing.current) return;
if (selectedShapeRef.current === "freeform") return;
canvas.isDrawingMode = false;
// get pointer coordinates
const pointer = canvas.getPointer(options.e);
// depending on the selected shape, set the dimensions of the shape stored in shapeRef in previous step of handelCanvasMouseDown
// calculate shape dimensions based on pointer coordinates
switch (selectedShapeRef?.current) {
case "rectangle":
shapeRef.current?.set({
width: pointer.x - (shapeRef.current?.left || 0),
height: pointer.y - (shapeRef.current?.top || 0),
});
break;
case "circle":
shapeRef.current.set({
radius: Math.abs(pointer.x - (shapeRef.current?.left || 0)) / 2,
});
break;
case "triangle":
shapeRef.current?.set({
width: pointer.x - (shapeRef.current?.left || 0),
height: pointer.y - (shapeRef.current?.top || 0),
});
break;
case "line":
shapeRef.current?.set({
x2: pointer.x,
y2: pointer.y,
});
break;
case "image":
shapeRef.current?.set({
width: pointer.x - (shapeRef.current?.left || 0),
height: pointer.y - (shapeRef.current?.top || 0),
});
default:
break;
}
// render objects on canvas
// renderAll: http://fabricjs.com/docs/fabric.Canvas.html#renderAll
canvas.renderAll();
// sync shape in storage
if (shapeRef.current?.objectId) {
syncShapeInStorage(shapeRef.current);
}
};
// handle mouse up event on canvas to stop drawing shapes
export const handleCanvasMouseUp = ({
canvas,
isDrawing,
shapeRef,
activeObjectRef,
selectedShapeRef,
syncShapeInStorage,
setActiveElement,
}: CanvasMouseUp) => {
isDrawing.current = false;
if (selectedShapeRef.current === "freeform") return;
// sync shape in storage as drawing is stopped
syncShapeInStorage(shapeRef.current);
// set everything to null
shapeRef.current = null;
activeObjectRef.current = null;
selectedShapeRef.current = null;
// if canvas is not in drawing mode, set active element to default nav element after 700ms
if (!canvas.isDrawingMode) {
setTimeout(() => {
setActiveElement(defaultNavElement);
}, 700);
}
};
// update shape in storage when object is modified
export const handleCanvasObjectModified = ({
options,
syncShapeInStorage,
}: CanvasObjectModified) => {
const target = options.target;
if (!target) return;
if (target?.type == "activeSelection") {
// fix this
} else {
syncShapeInStorage(target);
}
};
// update shape in storage when path is created when in freeform mode
export const handlePathCreated = ({
options,
syncShapeInStorage,
}: CanvasPathCreated) => {
// get path object
const path = options.path;
if (!path) return;
// set unique id to path object
path.set({
objectId: uuid4(),
});
// sync shape in storage
syncShapeInStorage(path);
};
// check how object is moving on canvas and restrict it to canvas boundaries
export const handleCanvasObjectMoving = ({
options,
}: {
options: fabric.IEvent;
}) => {
// get target object which is moving
const target = options.target as fabric.Object;
// target.canvas is the canvas on which the object is moving
const canvas = target.canvas as fabric.Canvas;
// set coordinates of target object
target.setCoords();
// restrict object to canvas boundaries (horizontal)
if (target && target.left) {
target.left = Math.max(
0,
Math.min(
target.left,
(canvas.width || 0) - (target.getScaledWidth() || target.width || 0)
)
);
}
// restrict object to canvas boundaries (vertical)
if (target && target.top) {
target.top = Math.max(
0,
Math.min(
target.top,
(canvas.height || 0) - (target.getScaledHeight() || target.height || 0)
)
);
}
};
// set element attributes when element is selected
export const handleCanvasSelectionCreated = ({
options,
isEditingRef,
setElementAttributes,
}: CanvasSelectionCreated) => {
// if user is editing manually, return
if (isEditingRef.current) return;
// if no element is selected, return
if (!options?.selected) return;
// get the selected element
const selectedElement = options?.selected[0] as fabric.Object;
// if only one element is selected, set element attributes
if (selectedElement && options.selected.length === 1) {
// calculate scaled dimensions of the object
const scaledWidth = selectedElement?.scaleX
? selectedElement?.width! * selectedElement?.scaleX
: selectedElement?.width;
const scaledHeight = selectedElement?.scaleY
? selectedElement?.height! * selectedElement?.scaleY
: selectedElement?.height;
setElementAttributes({
width: scaledWidth?.toFixed(0).toString() || "",
height: scaledHeight?.toFixed(0).toString() || "",
fill: selectedElement?.fill?.toString() || "",
stroke: selectedElement?.stroke || "",
// @ts-ignore
fontSize: selectedElement?.fontSize || "",
// @ts-ignore
fontFamily: selectedElement?.fontFamily || "",
// @ts-ignore
fontWeight: selectedElement?.fontWeight || "",
});
}
};
// update element attributes when element is scaled
export const handleCanvasObjectScaling = ({
options,
setElementAttributes,
}: CanvasObjectScaling) => {
const selectedElement = options.target;
// calculate scaled dimensions of the object
const scaledWidth = selectedElement?.scaleX
? selectedElement?.width! * selectedElement?.scaleX
: selectedElement?.width;
const scaledHeight = selectedElement?.scaleY
? selectedElement?.height! * selectedElement?.scaleY
: selectedElement?.height;
setElementAttributes((prev) => ({
...prev,
width: scaledWidth?.toFixed(0).toString() || "",
height: scaledHeight?.toFixed(0).toString() || "",
}));
};
// render canvas objects coming from storage on canvas
export const renderCanvas = ({
fabricRef,
canvasObjects,
activeObjectRef,
}: RenderCanvas) => {
// clear canvas
fabricRef.current?.clear();
// render all objects on canvas
Array.from(canvasObjects, ([objectId, objectData]) => {
/**
* enlivenObjects() is used to render objects on canvas.
* It takes two arguments:
* 1. objectData: object data to render on canvas
* 2. callback: callback function to execute after rendering objects
* on canvas
*
* enlivenObjects: http://fabricjs.com/docs/fabric.util.html#.enlivenObjectEnlivables
*/
fabric.util.enlivenObjects(
[objectData],
(enlivenedObjects: fabric.Object[]) => {
enlivenedObjects.forEach((enlivenedObj) => {
// if element is active, keep it in active state so that it can be edited further
if (activeObjectRef.current?.objectId === objectId) {
fabricRef.current?.setActiveObject(enlivenedObj);
}
// add object to canvas
fabricRef.current?.add(enlivenedObj);
});
},
/**
* specify namespace of the object for fabric to render it on canvas
* A namespace is a string that is used to identify the type of
* object.
*
* Fabric Namespace: http://fabricjs.com/docs/fabric.html
*/
"fabric"
);
});
fabricRef.current?.renderAll();
};
// resize canvas dimensions on window resize
export const handleResize = ({ canvas }: { canvas: fabric.Canvas | null }) => {
const canvasElement = document.getElementById("canvas");
if (!canvasElement) return;
if (!canvas) return;
canvas.setDimensions({
width: canvasElement.clientWidth,
height: canvasElement.clientHeight,
});
};
// zoom canvas on mouse scroll
export const handleCanvasZoom = ({
options,
canvas,
}: {
options: fabric.IEvent & { e: WheelEvent };
canvas: fabric.Canvas;
}) => {
const delta = options.e?.deltaY;
let zoom = canvas.getZoom();
// allow zooming to min 20% and max 100%
const minZoom = 0.2;
const maxZoom = 1;
const zoomStep = 0.001;
// calculate zoom based on mouse scroll wheel with min and max zoom
zoom = Math.min(Math.max(minZoom, zoom + delta * zoomStep), maxZoom);
// set zoom to canvas
// zoomToPoint: http://fabricjs.com/docs/fabric.Canvas.html#zoomToPoint
canvas.zoomToPoint({ x: options.e.offsetX, y: options.e.offsetY }, zoom);
options.e.preventDefault();
options.e.stopPropagation();
};