@mhamz.01/easyflow-whiteboard 2.19.0 → 2.21.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.
|
@@ -20,6 +20,6 @@ interface UseMouseHandlersProps {
|
|
|
20
20
|
handleEraserUp: () => boolean;
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
export declare const useMouseHandlers: ({ fabricCanvas, activeTool,
|
|
23
|
+
export declare const useMouseHandlers: ({ fabricCanvas, activeTool, drawingHandlers, eraserHandlers, }: UseMouseHandlersProps) => void;
|
|
24
24
|
export {};
|
|
25
25
|
//# sourceMappingURL=useMouseHandlers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMouseHandlers.d.ts","sourceRoot":"","sources":["../../src/hooks/useMouseHandlers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useMouseHandlers.d.ts","sourceRoot":"","sources":["../../src/hooks/useMouseHandlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,SAAS,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,UAAU,qBAAqB;IAC7B,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAI,MAAM,CAAC;IACrB,WAAW,EAAG,GAAG,CAAC;IAClB,eAAe,EAAE;QACf,eAAe,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;QACpC,eAAe,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;QACpC,aAAa,EAAI,MAAM,IAAI,CAAC;KAC7B,CAAC;IACF,cAAc,EAAE;QACd,eAAe,EAAG,MAAM,IAAI,CAAC;QAC7B,cAAc,EAAI,MAAM,IAAI,CAAC;QAC7B,gBAAgB,EAAE,MAAM,OAAO,CAAC;QAChC,gBAAgB,EAAE,CAAC,OAAO,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,CAAC;QAC9D,cAAc,EAAI,MAAM,OAAO,CAAC;KACjC,CAAC;CACH;AAED,eAAO,MAAM,gBAAgB,GAAI,gEAK9B,qBAAqB,SAqFvB,CAAC"}
|
|
@@ -1,44 +1,81 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
2
|
-
export const useMouseHandlers = ({ fabricCanvas, activeTool,
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
export const useMouseHandlers = ({ fabricCanvas, activeTool, drawingHandlers, eraserHandlers, }) => {
|
|
3
|
+
// ── PERF FIX 1: All props in refs — effect registers ONCE ────────────────
|
|
4
|
+
// Old deps: [activeTool, toolOptions, drawingHandlers, eraserHandlers]
|
|
5
|
+
// drawingHandlers and eraserHandlers are plain objects recreated every render
|
|
6
|
+
// → all 5 canvas listeners re-registered on every single render.
|
|
7
|
+
// With refs, the effect is stable for the full lifetime of the canvas.
|
|
8
|
+
const activeToolRef = useRef(activeTool);
|
|
9
|
+
const drawingRef = useRef(drawingHandlers);
|
|
10
|
+
const eraserRef = useRef(eraserHandlers);
|
|
11
|
+
// These update every render synchronously — handlers always see latest values
|
|
12
|
+
activeToolRef.current = activeTool;
|
|
13
|
+
drawingRef.current = drawingHandlers;
|
|
14
|
+
eraserRef.current = eraserHandlers;
|
|
3
15
|
useEffect(() => {
|
|
4
16
|
const canvas = fabricCanvas.current;
|
|
5
17
|
if (!canvas)
|
|
6
18
|
return;
|
|
19
|
+
// ── PERF FIX 2: rAF throttle on mouse:move ───────────────────────────
|
|
20
|
+
// mouse:move fires at raw pointer rate — up to 200-1000 events/sec on
|
|
21
|
+
// high-DPI displays. Without throttling, getScenePoint + handler logic
|
|
22
|
+
// runs far more often than the screen can paint (60-120fps).
|
|
23
|
+
let rafId = null;
|
|
24
|
+
let lastMoveOpt = null;
|
|
7
25
|
const handleMouseDown = (opt) => {
|
|
8
|
-
|
|
9
|
-
if (eraserHandlers.handleEraserDown())
|
|
26
|
+
if (eraserRef.current.handleEraserDown())
|
|
10
27
|
return;
|
|
11
|
-
|
|
12
|
-
drawingHandlers.handleMouseDown(opt);
|
|
28
|
+
drawingRef.current.handleMouseDown(opt);
|
|
13
29
|
};
|
|
14
30
|
const handleMouseMove = (opt) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
// Store latest event — rAF will consume it
|
|
32
|
+
lastMoveOpt = opt;
|
|
33
|
+
if (rafId !== null)
|
|
34
|
+
return; // frame already queued
|
|
35
|
+
rafId = requestAnimationFrame(() => {
|
|
36
|
+
rafId = null;
|
|
37
|
+
if (!lastMoveOpt)
|
|
38
|
+
return;
|
|
39
|
+
const currentOpt = lastMoveOpt;
|
|
40
|
+
lastMoveOpt = null;
|
|
41
|
+
const tool = activeToolRef.current;
|
|
42
|
+
if (tool === "eraser") {
|
|
43
|
+
// ── PERF FIX 3: Only call getScenePoint for tools that need it ────
|
|
44
|
+
const canvas = fabricCanvas.current;
|
|
45
|
+
if (!canvas)
|
|
46
|
+
return;
|
|
47
|
+
const pointer = canvas.getScenePoint(currentOpt.e);
|
|
48
|
+
eraserRef.current.handleEraserMove(pointer);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
drawingRef.current.handleMouseMove(currentOpt);
|
|
52
|
+
});
|
|
23
53
|
};
|
|
24
54
|
const handleMouseUp = () => {
|
|
25
|
-
//
|
|
26
|
-
if (
|
|
55
|
+
// Cancel any pending move frame on mouse up
|
|
56
|
+
if (rafId !== null) {
|
|
57
|
+
cancelAnimationFrame(rafId);
|
|
58
|
+
rafId = null;
|
|
59
|
+
lastMoveOpt = null;
|
|
60
|
+
}
|
|
61
|
+
if (eraserRef.current.handleEraserUp())
|
|
27
62
|
return;
|
|
28
|
-
|
|
29
|
-
drawingHandlers.handleMouseUp();
|
|
63
|
+
drawingRef.current.handleMouseUp();
|
|
30
64
|
};
|
|
31
|
-
canvas.on("mouse:over",
|
|
32
|
-
canvas.on("mouse:out",
|
|
65
|
+
canvas.on("mouse:over", () => eraserRef.current.handleMouseOver());
|
|
66
|
+
canvas.on("mouse:out", () => eraserRef.current.handleMouseOut());
|
|
33
67
|
canvas.on("mouse:down", handleMouseDown);
|
|
34
68
|
canvas.on("mouse:move", handleMouseMove);
|
|
35
69
|
canvas.on("mouse:up", handleMouseUp);
|
|
36
70
|
return () => {
|
|
37
|
-
canvas.off("mouse:over",
|
|
38
|
-
canvas.off("mouse:out",
|
|
71
|
+
canvas.off("mouse:over", () => eraserRef.current.handleMouseOver());
|
|
72
|
+
canvas.off("mouse:out", () => eraserRef.current.handleMouseOut());
|
|
39
73
|
canvas.off("mouse:down", handleMouseDown);
|
|
40
74
|
canvas.off("mouse:move", handleMouseMove);
|
|
41
75
|
canvas.off("mouse:up", handleMouseUp);
|
|
76
|
+
if (rafId !== null)
|
|
77
|
+
cancelAnimationFrame(rafId);
|
|
42
78
|
};
|
|
43
|
-
|
|
79
|
+
// ── PERF FIX 4: Stable — everything read via refs ─────────────────────────
|
|
80
|
+
}, [fabricCanvas]);
|
|
44
81
|
};
|
|
@@ -8,7 +8,7 @@ export declare const updateDrawingObject: (shape: FabricObject, toolType: string
|
|
|
8
8
|
y: number;
|
|
9
9
|
}) => void;
|
|
10
10
|
export declare const deleteSelectedObjects: (canvas: Canvas) => void;
|
|
11
|
-
export declare const exportCanvasToJSON: (
|
|
11
|
+
export declare const exportCanvasToJSON: (_canvas: Canvas) => void;
|
|
12
12
|
export declare const importCanvasFromJSON: (canvas: Canvas, json: string, callback?: () => void) => void;
|
|
13
13
|
export declare const exportCanvasToImage: (canvas: Canvas, format?: "png" | "jpeg") => string;
|
|
14
14
|
export declare const calculateDashArray: (strokeDashArray: number[] | null, strokeWidth: number) => number[] | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fabric-utils.d.ts","sourceRoot":"","sources":["../../src/lib/fabric-utils.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"fabric-utils.d.ts","sourceRoot":"","sources":["../../src/lib/fabric-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EAAE,YAAY,EACN,WAAW,EAC1B,MAAM,QAAQ,CAAC;AAMhB,eAAO,MAAM,sBAAsB,GAAI,QAAQ,MAAM,WAgBpD,CAAC;AAIF,eAAO,MAAM,mBAAmB,GAC9B,OAAc,YAAY,EAC1B,UAAc,MAAM,EACpB,YAAc;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACtC,cAAc;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,SAsDvC,CAAC;AAIF,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,SAOnD,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,SAEjD,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,WAAW,MAAM,IAAI,SAKvF,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,QAAQ,MAAM,EAAE,SAAQ,KAAK,GAAG,MAAc,WAEjF,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAC7B,iBAAiB,MAAM,EAAE,GAAG,IAAI,EAChC,aAAa,MAAM,KAClB,MAAM,EAAE,GAAG,SAIb,CAAC;AAIF,eAAO,MAAM,OAAO,GAClB,QAAQ,MAAM,EACd,SAAS;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CACzD,SAgBF,CAAC;AAIF,eAAO,MAAM,gBAAgB,GAAU,QAAQ,MAAM,EAAE,KAAK,MAAM,iJAoBjE,CAAC;AAIF,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,SAIzC,CAAC;AAIF,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,EAAE,WAAW,MAAM,SAG3D,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,SAqChD,CAAC;AAIF,eAAO,MAAM,iBAAiB,GAAU,QAAQ,MAAM,EAAE,SAAS,MAAM,kBAqCtE,CAAC"}
|
package/dist/lib/fabric-utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { FabricObject, Text, Line, Group, IText,
|
|
2
|
-
|
|
1
|
+
import { FabricObject, Text, Line, Group, IText, FabricImage, util, } from "fabric";
|
|
2
|
+
import { PencilBrush } from "fabric";
|
|
3
|
+
// ── Canvas initialization ─────────────────────────────────────────────────────
|
|
3
4
|
export const initializeFabricCanvas = (canvas) => {
|
|
4
|
-
// Set default object properties
|
|
5
5
|
FabricObject.prototype.set({
|
|
6
6
|
transparentCorners: false,
|
|
7
7
|
borderColor: "#3b82f6",
|
|
@@ -10,49 +10,40 @@ export const initializeFabricCanvas = (canvas) => {
|
|
|
10
10
|
cornerStyle: "circle",
|
|
11
11
|
padding: 4,
|
|
12
12
|
});
|
|
13
|
-
//
|
|
13
|
+
// Object caching — composites complex objects into a cached bitmap.
|
|
14
|
+
// Dramatically reduces repaint cost for objects with shadows, gradients, etc.
|
|
14
15
|
FabricObject.prototype.objectCaching = true;
|
|
15
|
-
// Initialize drawing brush
|
|
16
16
|
canvas.freeDrawingBrush = new PencilBrush(canvas);
|
|
17
17
|
return canvas;
|
|
18
18
|
};
|
|
19
|
-
//
|
|
19
|
+
// ── updateDrawingObject ───────────────────────────────────────────────────────
|
|
20
20
|
export const updateDrawingObject = (shape, toolType, startPoint, currentPoint) => {
|
|
21
|
-
const width = currentPoint.x - startPoint.x;
|
|
22
|
-
const height = currentPoint.y - startPoint.y;
|
|
23
21
|
switch (toolType) {
|
|
24
22
|
case "rectangle":
|
|
25
23
|
case "frame": {
|
|
26
|
-
const rect = shape;
|
|
24
|
+
const rect = shape;
|
|
27
25
|
const dx = currentPoint.x - startPoint.x;
|
|
28
26
|
const dy = currentPoint.y - startPoint.y;
|
|
29
|
-
// 1. Calculate the center point (Midpoint)
|
|
30
|
-
const midX = startPoint.x + dx / 2;
|
|
31
|
-
const midY = startPoint.y + dy / 2;
|
|
32
|
-
// 2. Calculate dimensions
|
|
33
|
-
// We subtract a tiny fraction of the strokeWidth if you want the
|
|
34
|
-
// outer visual edge to be exactly under the pixel of the cursor.
|
|
35
27
|
const strokeW = rect.strokeWidth || 0;
|
|
36
|
-
const width = Math.abs(dx) - strokeW;
|
|
37
|
-
const height = Math.abs(dy) - strokeW;
|
|
38
28
|
rect.set({
|
|
39
|
-
left:
|
|
40
|
-
top:
|
|
41
|
-
width: Math.max(0,
|
|
42
|
-
height: Math.max(0,
|
|
29
|
+
left: startPoint.x + dx / 2,
|
|
30
|
+
top: startPoint.y + dy / 2,
|
|
31
|
+
width: Math.max(0, Math.abs(dx) - strokeW),
|
|
32
|
+
height: Math.max(0, Math.abs(dy) - strokeW),
|
|
43
33
|
originX: "center",
|
|
44
34
|
originY: "center",
|
|
45
35
|
});
|
|
36
|
+
// ── PERF FIX 1: Single setCoords() call ──────────────────────────────
|
|
37
|
+
// Old code called setCoords() inside each case AND again at the bottom
|
|
38
|
+
// of the function — double call on every mousemove during drawing.
|
|
46
39
|
rect.setCoords();
|
|
47
|
-
|
|
40
|
+
return; // early return skips the bottom setCoords()
|
|
48
41
|
}
|
|
49
42
|
case "circle": {
|
|
50
43
|
const circle = shape;
|
|
51
44
|
const dx = currentPoint.x - startPoint.x;
|
|
52
45
|
const dy = currentPoint.y - startPoint.y;
|
|
53
|
-
// Radius from diagonal distance
|
|
54
46
|
const radius = Math.sqrt(dx * dx + dy * dy) / 2;
|
|
55
|
-
// Center at midpoint
|
|
56
47
|
circle.set({
|
|
57
48
|
left: startPoint.x + dx / 2,
|
|
58
49
|
top: startPoint.y + dy / 2,
|
|
@@ -61,61 +52,53 @@ export const updateDrawingObject = (shape, toolType, startPoint, currentPoint) =
|
|
|
61
52
|
originY: "center",
|
|
62
53
|
});
|
|
63
54
|
circle.setCoords();
|
|
64
|
-
|
|
55
|
+
return;
|
|
65
56
|
}
|
|
66
57
|
case "line":
|
|
67
|
-
case "arrow":
|
|
58
|
+
case "arrow": {
|
|
68
59
|
if (shape instanceof Line) {
|
|
69
60
|
shape.set({
|
|
70
|
-
x1: startPoint.x,
|
|
71
|
-
|
|
72
|
-
x2: currentPoint.x,
|
|
73
|
-
y2: currentPoint.y,
|
|
61
|
+
x1: startPoint.x, y1: startPoint.y,
|
|
62
|
+
x2: currentPoint.x, y2: currentPoint.y,
|
|
74
63
|
});
|
|
75
64
|
}
|
|
76
|
-
|
|
65
|
+
shape.setCoords();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
77
68
|
}
|
|
78
|
-
shape.setCoords();
|
|
79
69
|
};
|
|
80
|
-
//
|
|
70
|
+
// ── deleteSelectedObjects ─────────────────────────────────────────────────────
|
|
81
71
|
export const deleteSelectedObjects = (canvas) => {
|
|
82
72
|
const activeObjects = canvas.getActiveObjects();
|
|
83
|
-
if (activeObjects.length)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
73
|
+
if (!activeObjects.length)
|
|
74
|
+
return;
|
|
75
|
+
activeObjects.forEach((obj) => canvas.remove(obj));
|
|
76
|
+
canvas.discardActiveObject();
|
|
77
|
+
// ── PERF FIX 2: requestRenderAll instead of renderAll ─────────────────────
|
|
78
|
+
canvas.requestRenderAll();
|
|
90
79
|
};
|
|
91
|
-
// Export
|
|
92
|
-
export const exportCanvasToJSON = (
|
|
93
|
-
//
|
|
80
|
+
// ── Export / Import ───────────────────────────────────────────────────────────
|
|
81
|
+
export const exportCanvasToJSON = (_canvas) => {
|
|
82
|
+
// Intentionally left as a no-op stub — implement when needed
|
|
94
83
|
};
|
|
95
|
-
// Import canvas from JSON
|
|
96
84
|
export const importCanvasFromJSON = (canvas, json, callback) => {
|
|
97
85
|
canvas.loadFromJSON(json).then(() => {
|
|
98
|
-
canvas.
|
|
86
|
+
canvas.requestRenderAll(); // PERF FIX 3
|
|
99
87
|
if (callback)
|
|
100
88
|
callback();
|
|
101
89
|
});
|
|
102
90
|
};
|
|
103
|
-
// Export canvas as image
|
|
104
91
|
export const exportCanvasToImage = (canvas, format = "png") => {
|
|
105
|
-
return canvas.toDataURL({
|
|
106
|
-
format,
|
|
107
|
-
quality: 1,
|
|
108
|
-
multiplier: 2, // 2x resolution for better quality
|
|
109
|
-
});
|
|
92
|
+
return canvas.toDataURL({ format, quality: 1, multiplier: 2 });
|
|
110
93
|
};
|
|
111
|
-
//
|
|
94
|
+
// ── calculateDashArray ────────────────────────────────────────────────────────
|
|
112
95
|
export const calculateDashArray = (strokeDashArray, strokeWidth) => {
|
|
113
96
|
if (!strokeDashArray)
|
|
114
97
|
return undefined;
|
|
115
98
|
const multiplier = Math.max(strokeWidth, 1);
|
|
116
|
-
return strokeDashArray.map(
|
|
99
|
+
return strokeDashArray.map((v) => v * multiplier);
|
|
117
100
|
};
|
|
118
|
-
//
|
|
101
|
+
// ── addText ───────────────────────────────────────────────────────────────────
|
|
119
102
|
export const addText = (canvas, options) => {
|
|
120
103
|
const text = new IText(options.text || "Double click to edit", {
|
|
121
104
|
left: options.x || 100,
|
|
@@ -124,147 +107,108 @@ export const addText = (canvas, options) => {
|
|
|
124
107
|
fontFamily: options.fontFamily || "Arial",
|
|
125
108
|
fontWeight: options.fontWeight || "400",
|
|
126
109
|
fill: options.color || "#000000",
|
|
127
|
-
// textAlign: options.textAlign || "left",
|
|
128
110
|
});
|
|
129
111
|
canvas.add(text);
|
|
130
112
|
canvas.setActiveObject(text);
|
|
131
|
-
canvas.
|
|
132
|
-
// Auto-enter edit mode
|
|
113
|
+
canvas.requestRenderAll(); // PERF FIX 4
|
|
133
114
|
text.enterEditing();
|
|
134
115
|
text.selectAll();
|
|
135
116
|
};
|
|
117
|
+
// ── addImageToCanvas ──────────────────────────────────────────────────────────
|
|
136
118
|
export const addImageToCanvas = async (canvas, url) => {
|
|
137
119
|
if (!canvas)
|
|
138
120
|
return;
|
|
139
|
-
// 1. Get the current Viewport Transform (the Pan and Zoom matrix)
|
|
140
121
|
const vpt = canvas.viewportTransform;
|
|
141
122
|
const zoom = canvas.getZoom();
|
|
142
|
-
// 2. Calculate the exact Scene coordinates for the center
|
|
143
|
-
// Formula: (-PanOffset / Zoom) + (CanvasDimension / 2 / Zoom)
|
|
144
123
|
const centerX = (-vpt[4] + canvas.width / 2) / zoom;
|
|
145
124
|
const centerY = (-vpt[5] + canvas.height / 2) / zoom;
|
|
146
125
|
try {
|
|
147
126
|
const img = await FabricImage.fromURL(url);
|
|
148
|
-
// 3. Dynamic Scaling (Make it fit roughly 40% of the screen height)
|
|
149
127
|
const targetHeight = (canvas.height / zoom) * 0.4;
|
|
150
128
|
img.scaleToHeight(targetHeight);
|
|
151
|
-
img.set({
|
|
152
|
-
left: centerX,
|
|
153
|
-
top: centerY,
|
|
154
|
-
originX: "center",
|
|
155
|
-
originY: "center",
|
|
156
|
-
// cornerStyle: "circle",
|
|
157
|
-
// transparentCorners: false,
|
|
158
|
-
// cornerColor: "#3b82f6", // Nice modern blue
|
|
159
|
-
// cornerStrokeColor: "#ffffff",
|
|
160
|
-
padding: 10
|
|
161
|
-
});
|
|
129
|
+
img.set({ left: centerX, top: centerY, originX: "center", originY: "center", padding: 10 });
|
|
162
130
|
canvas.add(img);
|
|
163
131
|
canvas.setActiveObject(img);
|
|
164
|
-
canvas.
|
|
132
|
+
canvas.requestRenderAll(); // PERF FIX 5
|
|
165
133
|
return img;
|
|
166
134
|
}
|
|
167
135
|
catch (error) {
|
|
168
136
|
console.error("Image load failed", error);
|
|
169
137
|
}
|
|
170
138
|
};
|
|
171
|
-
//
|
|
139
|
+
// ── clearCanvas ───────────────────────────────────────────────────────────────
|
|
172
140
|
export const clearCanvas = (canvas) => {
|
|
173
141
|
canvas.clear();
|
|
174
142
|
canvas.backgroundColor = "transparent";
|
|
175
|
-
canvas.
|
|
143
|
+
canvas.requestRenderAll(); // PERF FIX 6
|
|
176
144
|
};
|
|
177
|
-
//
|
|
145
|
+
// ── zoomCanvas ────────────────────────────────────────────────────────────────
|
|
178
146
|
export const zoomCanvas = (canvas, zoomLevel) => {
|
|
179
147
|
canvas.setZoom(zoomLevel);
|
|
180
|
-
canvas.
|
|
148
|
+
canvas.requestRenderAll(); // PERF FIX 7
|
|
181
149
|
};
|
|
182
|
-
//
|
|
150
|
+
// ── fitCanvasToObjects ────────────────────────────────────────────────────────
|
|
183
151
|
export const fitCanvasToObjects = (canvas) => {
|
|
184
152
|
const objects = canvas.getObjects();
|
|
185
|
-
if (objects.length
|
|
153
|
+
if (!objects.length)
|
|
186
154
|
return;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
155
|
+
// ── PERF FIX 8: Use getBoundingRect instead of creating a Group ───────────
|
|
156
|
+
// Old code: new Group(objects) — instantiates a full Fabric Group object
|
|
157
|
+
// with layout recalculation for every object just to get bounds. O(n) object
|
|
158
|
+
// creation + layout for a measurement-only operation.
|
|
159
|
+
// getBoundingRect() computes bounds from existing coords — no allocation.
|
|
160
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
161
|
+
for (const obj of objects) {
|
|
162
|
+
const bounds = obj.getBoundingRect();
|
|
163
|
+
if (bounds.left < minX)
|
|
164
|
+
minX = bounds.left;
|
|
165
|
+
if (bounds.top < minY)
|
|
166
|
+
minY = bounds.top;
|
|
167
|
+
if (bounds.left + bounds.width > maxX)
|
|
168
|
+
maxX = bounds.left + bounds.width;
|
|
169
|
+
if (bounds.top + bounds.height > maxY)
|
|
170
|
+
maxY = bounds.top + bounds.height;
|
|
171
|
+
}
|
|
172
|
+
const contentW = maxX - minX;
|
|
173
|
+
const contentH = maxY - minY;
|
|
174
|
+
const padding = 100;
|
|
175
|
+
const zoom = Math.min(canvas.getWidth() / (contentW + padding), canvas.getHeight() / (contentH + padding));
|
|
193
176
|
canvas.setZoom(zoom);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
177
|
+
// Center the content
|
|
178
|
+
const vpt = canvas.viewportTransform;
|
|
179
|
+
if (vpt) {
|
|
180
|
+
vpt[4] = (canvas.getWidth() - contentW * zoom) / 2 - minX * zoom;
|
|
181
|
+
vpt[5] = (canvas.getHeight() - contentH * zoom) / 2 - minY * zoom;
|
|
182
|
+
}
|
|
183
|
+
canvas.requestRenderAll();
|
|
197
184
|
};
|
|
185
|
+
// ── addWelcomeContent ─────────────────────────────────────────────────────────
|
|
198
186
|
export const addWelcomeContent = async (canvas, logoUrl) => {
|
|
199
187
|
const centerX = canvas.getWidth() / 2;
|
|
200
188
|
const centerY = canvas.getHeight() / 2;
|
|
201
|
-
|
|
202
|
-
const logoImg = await FabricImage.fromURL(logoUrl, {
|
|
203
|
-
crossOrigin: 'anonymous'
|
|
204
|
-
});
|
|
205
|
-
// Scale logo to fit (e.g., 80px width)
|
|
189
|
+
const logoImg = await FabricImage.fromURL(logoUrl, { crossOrigin: "anonymous" });
|
|
206
190
|
const scale = 60 / logoImg.width;
|
|
207
|
-
logoImg.set({
|
|
208
|
-
scaleX: scale,
|
|
209
|
-
scaleY: scale,
|
|
210
|
-
originX: "center",
|
|
211
|
-
originY: "center",
|
|
212
|
-
top: -120, // Relative to group center
|
|
213
|
-
});
|
|
214
|
-
// 2. Creative Background "Aura" (The Glow)
|
|
215
|
-
// const aura = new Circle({
|
|
216
|
-
// radius: 100,
|
|
217
|
-
// fill: "radial-gradient(circle, rgba(99, 102, 241, 0.15) 0%, rgba(255, 255, 255, 0) 70%)",
|
|
218
|
-
// originX: "center",
|
|
219
|
-
// originY: "center",
|
|
220
|
-
// top: -120,
|
|
221
|
-
// selectable: false,
|
|
222
|
-
// evented: false,
|
|
223
|
-
// });
|
|
224
|
-
// 3. High-End Typography
|
|
191
|
+
logoImg.set({ scaleX: scale, scaleY: scale, originX: "center", originY: "center", top: -120 });
|
|
225
192
|
const mainTitle = new Text("EasyFlow", {
|
|
226
|
-
top: -20,
|
|
227
|
-
|
|
228
|
-
fontFamily: "'cursive', 'Segoe UI', Roboto, sans-serif",
|
|
229
|
-
fontWeight: "700",
|
|
230
|
-
fill: "#029AFF",
|
|
231
|
-
charSpacing: -20, // Tight kerning for a modern look
|
|
232
|
-
originX: "center",
|
|
193
|
+
top: -20, fontSize: 84, fontFamily: "'cursive', 'Segoe UI', Roboto, sans-serif",
|
|
194
|
+
fontWeight: "700", fill: "#029AFF", charSpacing: -20, originX: "center",
|
|
233
195
|
});
|
|
234
196
|
const subTitle = new Text("Whiteboard", {
|
|
235
|
-
top: 55,
|
|
236
|
-
|
|
237
|
-
fontFamily: "'Inter', 'Segoe UI', Roboto, sans-serif",
|
|
238
|
-
fontWeight: "300", // Thin contrast against the bold "EasyFlow"
|
|
239
|
-
fill: "#fff",
|
|
240
|
-
originX: "center",
|
|
197
|
+
top: 55, fontSize: 38, fontFamily: "'Inter', 'Segoe UI', Roboto, sans-serif",
|
|
198
|
+
fontWeight: "300", fill: "#fff", originX: "center",
|
|
241
199
|
});
|
|
242
200
|
const hint = new Text("SELECT A TOOL TO START CREATING", {
|
|
243
|
-
top: 110,
|
|
244
|
-
|
|
245
|
-
fontFamily: "monospace", // Techy/Creative feel
|
|
246
|
-
fontWeight: "600",
|
|
247
|
-
fill: "#9ca3af",
|
|
248
|
-
charSpacing: 200, // Wide spacing for the hint
|
|
249
|
-
originX: "center",
|
|
201
|
+
top: 110, fontSize: 12, fontFamily: "monospace",
|
|
202
|
+
fontWeight: "600", fill: "#9ca3af", charSpacing: 200, originX: "center",
|
|
250
203
|
});
|
|
251
|
-
// 4. Create Master Hero Group
|
|
252
204
|
const heroGroup = new Group([logoImg, mainTitle, subTitle, hint], {
|
|
253
|
-
left: centerX,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
originY: "center",
|
|
257
|
-
selectable: false,
|
|
258
|
-
evented: false,
|
|
259
|
-
opacity: 0,
|
|
205
|
+
left: centerX, top: centerY + 20,
|
|
206
|
+
originX: "center", originY: "center",
|
|
207
|
+
selectable: false, evented: false, opacity: 0,
|
|
260
208
|
});
|
|
261
209
|
heroGroup.id = "welcome-hero";
|
|
262
210
|
canvas.add(heroGroup);
|
|
263
|
-
|
|
264
|
-
heroGroup.animate({
|
|
265
|
-
opacity: 1,
|
|
266
|
-
top: centerY // Moves up 20px while fading in
|
|
267
|
-
}, {
|
|
211
|
+
heroGroup.animate({ opacity: 1, top: centerY }, {
|
|
268
212
|
duration: 1200,
|
|
269
213
|
easing: util.ease.easeOutQuart,
|
|
270
214
|
onChange: () => canvas.requestRenderAll(),
|