@mhamz.01/easyflow-whiteboard 2.18.0 → 2.20.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/dist/hooks/useDrawing.d.ts.map +1 -1
- package/dist/hooks/useDrawing.js +139 -134
- package/dist/hooks/useMouseHandlers.d.ts +1 -1
- package/dist/hooks/useMouseHandlers.d.ts.map +1 -1
- package/dist/hooks/useMouseHandlers.js +60 -23
- package/dist/hooks/usePan.d.ts.map +1 -1
- package/dist/hooks/usePan.js +27 -59
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDrawing.d.ts","sourceRoot":"","sources":["../../src/hooks/useDrawing.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useDrawing.d.ts","sourceRoot":"","sources":["../../src/hooks/useDrawing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,SAAS,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,iBAAiB,EAAsB,MAAM,QAAQ,CAAC;AAMrF,UAAU,eAAe;IACvB,YAAY,EAAQ,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,UAAU,EAAU,MAAM,CAAC;IAC3B,WAAW,EAAS,GAAG,CAAC;IACxB,YAAY,EAAQ,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,aAAa,EAAO,SAAS,CAAC;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAC/D,eAAe,EAAK,SAAS,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IACnD,kBAAkB,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,WAAW,EAAS,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,eAAe,EAAK,CAAC,GAAG,EAAE,YAAY,KAAK,IAAI,CAAC;CACjD;AAED,eAAO,MAAM,UAAU,GAAI,4IAUxB,eAAe;2BAmBS,iBAAiB;2BAmGjB,iBAAiB;;CA0C3C,CAAC"}
|
package/dist/hooks/useDrawing.js
CHANGED
|
@@ -1,142 +1,147 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
1
2
|
import { Rect, Circle, Line } from "fabric";
|
|
2
3
|
import { Frame } from "../lib/fabric-frame";
|
|
3
4
|
import { Arrow } from "../lib/fabric-arrow";
|
|
4
5
|
import { calculateDashArray, updateDrawingObject, addText } from "../lib/fabric-utils";
|
|
5
6
|
import { useWhiteboardStore } from "../store/whiteboard-store";
|
|
6
7
|
export const useDrawing = ({ fabricCanvas, activeTool, toolOptions, isDrawingRef, startPointRef, currentShapeRef, suppressHistoryRef, pushHistory, addCanvasObject, }) => {
|
|
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
|
-
height: 1,
|
|
42
|
-
fill: toolOptions.rectangle.fillColor === "transparent"
|
|
43
|
-
? "transparent"
|
|
44
|
-
: toolOptions.rectangle.fillColor,
|
|
45
|
-
stroke: toolOptions.rectangle.strokeColor,
|
|
46
|
-
strokeWidth: toolOptions.rectangle.strokeWidth,
|
|
47
|
-
strokeDashArray: calculateDashArray(toolOptions.rectangle.strokeDashArray, toolOptions.rectangle.strokeWidth),
|
|
48
|
-
rx: 5,
|
|
49
|
-
ry: 5,
|
|
50
|
-
strokeLineCap: "round",
|
|
51
|
-
strokeLineJoin: "round",
|
|
52
|
-
selectable: false,
|
|
53
|
-
});
|
|
54
|
-
break;
|
|
55
|
-
case "circle":
|
|
56
|
-
shape = new Circle({
|
|
57
|
-
left: pointer.x,
|
|
58
|
-
top: pointer.y,
|
|
59
|
-
radius: 1,
|
|
60
|
-
fill: toolOptions.circle.fillColor === "transparent"
|
|
61
|
-
? "transparent"
|
|
62
|
-
: toolOptions.circle.fillColor,
|
|
63
|
-
stroke: toolOptions.circle.strokeColor,
|
|
64
|
-
strokeWidth: toolOptions.circle.strokeWidth,
|
|
65
|
-
strokeDashArray: toolOptions.circle.strokeDashArray
|
|
66
|
-
? [...toolOptions.circle.strokeDashArray]
|
|
67
|
-
: undefined,
|
|
68
|
-
selectable: false,
|
|
69
|
-
});
|
|
70
|
-
break;
|
|
71
|
-
case "frame":
|
|
72
|
-
shape = new Frame({
|
|
73
|
-
left: pointer.x,
|
|
74
|
-
top: pointer.y,
|
|
75
|
-
width: 1,
|
|
76
|
-
height: 1,
|
|
77
|
-
stroke: toolOptions.frame.strokeColor,
|
|
78
|
-
strokeWidth: toolOptions.frame.strokeWidth,
|
|
79
|
-
strokeDashArray: toolOptions.frame.strokeDashArray
|
|
80
|
-
? [...toolOptions.frame.strokeDashArray]
|
|
81
|
-
: undefined,
|
|
82
|
-
fill: toolOptions.frame.fillColor,
|
|
83
|
-
selectable: false,
|
|
8
|
+
// ── PERF FIX 1: Stable refs for all props ────────────────────────────────
|
|
9
|
+
// These plain handler functions are recreated every render. useMouseHandlers
|
|
10
|
+
// puts drawingHandlers in its deps array, causing all 5 canvas listeners to
|
|
11
|
+
// re-register on every render. Wrapping in refs makes the returned object
|
|
12
|
+
// referentially stable — useMouseHandlers can safely use [] deps.
|
|
13
|
+
const activeToolRef = useRef(activeTool);
|
|
14
|
+
const toolOptionsRef = useRef(toolOptions);
|
|
15
|
+
const pushHistoryRef = useRef(pushHistory);
|
|
16
|
+
const addCanvasObjectRef = useRef(addCanvasObject);
|
|
17
|
+
activeToolRef.current = activeTool;
|
|
18
|
+
toolOptionsRef.current = toolOptions;
|
|
19
|
+
pushHistoryRef.current = pushHistory;
|
|
20
|
+
addCanvasObjectRef.current = addCanvasObject;
|
|
21
|
+
// ── Stable handler refs — identity never changes ──────────────────────────
|
|
22
|
+
const handlersRef = useRef({
|
|
23
|
+
handleMouseDown: (opt) => {
|
|
24
|
+
const canvas = fabricCanvas.current;
|
|
25
|
+
if (!canvas)
|
|
26
|
+
return;
|
|
27
|
+
const tool = activeToolRef.current;
|
|
28
|
+
const options = toolOptionsRef.current;
|
|
29
|
+
const pointer = canvas.getScenePoint(opt.e);
|
|
30
|
+
if (opt.target || canvas.getActiveObject())
|
|
31
|
+
return;
|
|
32
|
+
// Text tool
|
|
33
|
+
if (tool === "text") {
|
|
34
|
+
addText(canvas, {
|
|
35
|
+
x: pointer.x,
|
|
36
|
+
y: pointer.y,
|
|
37
|
+
fontSize: options.text.fontSize,
|
|
38
|
+
fontFamily: options.text.fontFamily,
|
|
39
|
+
fontWeight: options.text.fontWeight,
|
|
40
|
+
color: options.text.color,
|
|
41
|
+
textAlign: options.text.textAlign,
|
|
84
42
|
});
|
|
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
|
-
|
|
43
|
+
pushHistoryRef.current(JSON.stringify(canvas.toJSON()));
|
|
44
|
+
useWhiteboardStore.getState().setActiveTool("select");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!["rectangle", "circle", "frame", "line", "arrow"].includes(tool))
|
|
48
|
+
return;
|
|
49
|
+
suppressHistoryRef.current = true;
|
|
50
|
+
isDrawingRef.current = true;
|
|
51
|
+
startPointRef.current = { x: pointer.x, y: pointer.y };
|
|
52
|
+
let shape = null;
|
|
53
|
+
switch (tool) {
|
|
54
|
+
case "rectangle":
|
|
55
|
+
shape = new Rect({
|
|
56
|
+
left: pointer.x, top: pointer.y, width: 1, height: 1,
|
|
57
|
+
fill: options.rectangle.fillColor === "transparent" ? "transparent" : options.rectangle.fillColor,
|
|
58
|
+
stroke: options.rectangle.strokeColor,
|
|
59
|
+
strokeWidth: options.rectangle.strokeWidth,
|
|
60
|
+
strokeDashArray: calculateDashArray(options.rectangle.strokeDashArray, options.rectangle.strokeWidth),
|
|
61
|
+
rx: 5, ry: 5,
|
|
62
|
+
strokeLineCap: "round", strokeLineJoin: "round",
|
|
63
|
+
selectable: false,
|
|
64
|
+
});
|
|
65
|
+
break;
|
|
66
|
+
case "circle":
|
|
67
|
+
shape = new Circle({
|
|
68
|
+
left: pointer.x, top: pointer.y, radius: 1,
|
|
69
|
+
fill: options.circle.fillColor === "transparent" ? "transparent" : options.circle.fillColor,
|
|
70
|
+
stroke: options.circle.strokeColor,
|
|
71
|
+
strokeWidth: options.circle.strokeWidth,
|
|
72
|
+
strokeDashArray: options.circle.strokeDashArray ? [...options.circle.strokeDashArray] : undefined,
|
|
73
|
+
selectable: false,
|
|
74
|
+
});
|
|
75
|
+
break;
|
|
76
|
+
case "frame":
|
|
77
|
+
shape = new Frame({
|
|
78
|
+
left: pointer.x, top: pointer.y, width: 1, height: 1,
|
|
79
|
+
stroke: options.frame.strokeColor,
|
|
80
|
+
strokeWidth: options.frame.strokeWidth,
|
|
81
|
+
strokeDashArray: options.frame.strokeDashArray ? [...options.frame.strokeDashArray] : undefined,
|
|
82
|
+
fill: options.frame.fillColor,
|
|
83
|
+
selectable: false,
|
|
84
|
+
});
|
|
85
|
+
break;
|
|
86
|
+
case "line":
|
|
87
|
+
shape = new Line([pointer.x, pointer.y, pointer.x, pointer.y], {
|
|
88
|
+
stroke: options.line.strokeColor,
|
|
89
|
+
strokeWidth: options.line.strokeWidth,
|
|
90
|
+
strokeDashArray: options.line.strokeDashArray || undefined,
|
|
91
|
+
selectable: false,
|
|
92
|
+
});
|
|
93
|
+
break;
|
|
94
|
+
case "arrow":
|
|
95
|
+
shape = new Arrow([pointer.x, pointer.y, pointer.x, pointer.y], {
|
|
96
|
+
stroke: options.arrow.strokeColor,
|
|
97
|
+
strokeWidth: options.arrow.strokeWidth,
|
|
98
|
+
strokeDashArray: options.arrow.strokeDashArray || undefined,
|
|
99
|
+
selectable: false,
|
|
100
|
+
});
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
if (shape) {
|
|
104
|
+
canvas.add(shape);
|
|
105
|
+
currentShapeRef.current = shape;
|
|
106
|
+
// ── PERF FIX 2: requestRenderAll instead of renderAll ─────────────
|
|
107
|
+
// renderAll() is synchronous — blocks the main thread immediately.
|
|
108
|
+
// requestRenderAll() schedules the paint on Fabric's rAF loop.
|
|
109
|
+
canvas.requestRenderAll();
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
handleMouseMove: (opt) => {
|
|
113
|
+
const canvas = fabricCanvas.current;
|
|
114
|
+
if (!canvas || !isDrawingRef.current || !currentShapeRef.current || !startPointRef.current)
|
|
115
|
+
return;
|
|
116
|
+
const pointer = canvas.getScenePoint(opt.e);
|
|
117
|
+
updateDrawingObject(currentShapeRef.current, activeToolRef.current, startPointRef.current, pointer);
|
|
118
|
+
// ── PERF FIX 3: requestRenderAll instead of renderAll ─────────────────
|
|
119
|
+
// This fires on every mousemove during drawing — the single most frequent
|
|
120
|
+
// call site. Synchronous renderAll() here directly causes frame drops.
|
|
121
|
+
canvas.requestRenderAll();
|
|
122
|
+
},
|
|
123
|
+
handleMouseUp: () => {
|
|
124
|
+
const canvas = fabricCanvas.current;
|
|
125
|
+
if (!canvas || !isDrawingRef.current || !currentShapeRef.current)
|
|
126
|
+
return;
|
|
127
|
+
const shape = currentShapeRef.current;
|
|
128
|
+
shape.set({ selectable: true, evented: true });
|
|
129
|
+
if (shape instanceof Frame)
|
|
130
|
+
canvas.sendObjectToBack(shape);
|
|
131
|
+
addCanvasObjectRef.current(shape);
|
|
132
|
+
suppressHistoryRef.current = false;
|
|
133
|
+
pushHistoryRef.current(JSON.stringify(canvas.toJSON()));
|
|
134
|
+
isDrawingRef.current = false;
|
|
135
|
+
startPointRef.current = null;
|
|
136
|
+
currentShapeRef.current = null;
|
|
137
|
+
useWhiteboardStore.getState().setActiveTool("select");
|
|
138
|
+
canvas.discardActiveObject();
|
|
139
|
+
// ── PERF FIX 4: Single requestRenderAll instead of two renderAll() ────
|
|
140
|
+
// Old code called renderAll() twice (after discardActiveObject + at end).
|
|
141
|
+
// One requestRenderAll() covers both.
|
|
142
|
+
canvas.requestRenderAll();
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
// Return stable object reference — identity never changes across renders
|
|
146
|
+
return handlersRef.current;
|
|
142
147
|
};
|
|
@@ -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
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePan.d.ts","sourceRoot":"","sources":["../../src/hooks/usePan.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"usePan.d.ts","sourceRoot":"","sources":["../../src/hooks/usePan.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,UAAU,WAAW;IACnB,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,iBAAiB,EAAE,CAAC,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACjE;AAED,eAAO,MAAM,MAAM,GAAI,8DAKpB,WAAW,SAkGb,CAAC"}
|
package/dist/hooks/usePan.js
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
import { useEffect
|
|
1
|
+
import { useEffect } from "react";
|
|
2
2
|
export const usePan = ({ fabricCanvas, activeTool, handleZoom, setCanvasViewport, }) => {
|
|
3
|
-
// ── PERF FIX 1: All mutable values in refs — effect registers ONCE ────────
|
|
4
|
-
// Old deps: [activeTool, handleZoom, setCanvasViewport, fabricCanvas]
|
|
5
|
-
// handleZoom and setCanvasViewport are new references every render →
|
|
6
|
-
// all 3 listeners re-registered on every render.
|
|
7
|
-
const activeToolRef = useRef(activeTool);
|
|
8
|
-
const handleZoomRef = useRef(handleZoom);
|
|
9
|
-
const setViewportRef = useRef(setCanvasViewport);
|
|
10
|
-
activeToolRef.current = activeTool;
|
|
11
|
-
handleZoomRef.current = handleZoom;
|
|
12
|
-
setViewportRef.current = setCanvasViewport;
|
|
13
3
|
useEffect(() => {
|
|
14
4
|
const canvas = fabricCanvas.current;
|
|
15
5
|
if (!canvas)
|
|
@@ -20,18 +10,17 @@ export const usePan = ({ fabricCanvas, activeTool, handleZoom, setCanvasViewport
|
|
|
20
10
|
let lastX = 0;
|
|
21
11
|
let lastY = 0;
|
|
22
12
|
let lastTouchDistance = 0;
|
|
23
|
-
let rafId = null;
|
|
24
13
|
const onDown = (opt) => {
|
|
25
|
-
if (
|
|
14
|
+
if (activeTool !== "pan")
|
|
26
15
|
return;
|
|
27
16
|
const e = opt.e;
|
|
28
|
-
// Pinch
|
|
29
|
-
if (e.touches
|
|
17
|
+
// Pinch initialization
|
|
18
|
+
if (e.touches && e.touches.length === 2) {
|
|
30
19
|
isPanning = false;
|
|
31
20
|
lastTouchDistance = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
|
|
32
21
|
return;
|
|
33
22
|
}
|
|
34
|
-
// Pan
|
|
23
|
+
// Pan initialization
|
|
35
24
|
const pointer = e.touches ? e.touches[0] : e;
|
|
36
25
|
isPanning = true;
|
|
37
26
|
lastX = pointer.clientX;
|
|
@@ -39,63 +28,45 @@ export const usePan = ({ fabricCanvas, activeTool, handleZoom, setCanvasViewport
|
|
|
39
28
|
canvas.setCursor("grabbing");
|
|
40
29
|
};
|
|
41
30
|
const onMove = (opt) => {
|
|
42
|
-
if (
|
|
31
|
+
if (activeTool !== "pan")
|
|
43
32
|
return;
|
|
44
33
|
const e = opt.e;
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
// requestRenderAll, saturating the main thread.
|
|
48
|
-
if (rafId !== null)
|
|
49
|
-
return; // skip if a frame is already queued
|
|
50
|
-
// Pinch zoom
|
|
51
|
-
if (e.touches?.length === 2) {
|
|
34
|
+
// Handle pinch zoom (two fingers)
|
|
35
|
+
if (e.touches && e.touches.length === 2) {
|
|
52
36
|
const currentDistance = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
|
|
53
37
|
if (lastTouchDistance > 0) {
|
|
38
|
+
const zoom = canvas.getZoom();
|
|
54
39
|
const delta = (currentDistance - lastTouchDistance) * 0.01;
|
|
40
|
+
const newZoom = zoom + delta;
|
|
55
41
|
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
|
56
42
|
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
|
57
43
|
const rect = canvasEl.getBoundingClientRect();
|
|
58
|
-
|
|
44
|
+
handleZoom(newZoom, { x: midX - rect.left, y: midY - rect.top });
|
|
59
45
|
}
|
|
60
46
|
lastTouchDistance = currentDistance;
|
|
61
47
|
return;
|
|
62
48
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const dx = pointer.clientX - lastX;
|
|
67
|
-
const dy = pointer.clientY - lastY;
|
|
68
|
-
lastX = pointer.clientX;
|
|
69
|
-
lastY = pointer.clientY;
|
|
70
|
-
// Schedule the actual VPT mutation on next animation frame
|
|
71
|
-
rafId = requestAnimationFrame(() => {
|
|
72
|
-
rafId = null;
|
|
49
|
+
// Handle panning (one finger or mouse)
|
|
50
|
+
if (isPanning) {
|
|
51
|
+
const pointer = e.touches ? e.touches[0] : e;
|
|
73
52
|
const vpt = canvas.viewportTransform;
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// Viewport state is only flushed ONCE on mouse:up (see onUp below).
|
|
83
|
-
});
|
|
53
|
+
if (vpt) {
|
|
54
|
+
vpt[4] += pointer.clientX - lastX;
|
|
55
|
+
vpt[5] += pointer.clientY - lastY;
|
|
56
|
+
canvas.requestRenderAll();
|
|
57
|
+
}
|
|
58
|
+
lastX = pointer.clientX;
|
|
59
|
+
lastY = pointer.clientY;
|
|
60
|
+
}
|
|
84
61
|
};
|
|
85
62
|
const onUp = () => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
63
|
+
const vpt = canvas.viewportTransform;
|
|
64
|
+
if (vpt) {
|
|
65
|
+
setCanvasViewport({ x: vpt[4], y: vpt[5] });
|
|
89
66
|
}
|
|
90
67
|
isPanning = false;
|
|
91
68
|
lastTouchDistance = 0;
|
|
92
|
-
canvas.setCursor(
|
|
93
|
-
// ── Flush React state ONCE per gesture end ────────────────────────────
|
|
94
|
-
// HTML overlay nodes only need to reposition when the pan is complete.
|
|
95
|
-
// One setCanvasViewport call here vs hundreds during the gesture.
|
|
96
|
-
const vpt = canvas.viewportTransform;
|
|
97
|
-
if (vpt)
|
|
98
|
-
setViewportRef.current({ x: vpt[4], y: vpt[5] });
|
|
69
|
+
canvas.setCursor(activeTool === "pan" ? "grab" : "default");
|
|
99
70
|
};
|
|
100
71
|
canvas.on("mouse:down", onDown);
|
|
101
72
|
canvas.on("mouse:move", onMove);
|
|
@@ -104,9 +75,6 @@ export const usePan = ({ fabricCanvas, activeTool, handleZoom, setCanvasViewport
|
|
|
104
75
|
canvas.off("mouse:down", onDown);
|
|
105
76
|
canvas.off("mouse:move", onMove);
|
|
106
77
|
canvas.off("mouse:up", onUp);
|
|
107
|
-
if (rafId !== null)
|
|
108
|
-
cancelAnimationFrame(rafId);
|
|
109
78
|
};
|
|
110
|
-
|
|
111
|
-
}, [fabricCanvas]);
|
|
79
|
+
}, [activeTool, handleZoom, setCanvasViewport, fabricCanvas]);
|
|
112
80
|
};
|