@deepnoid/canvas 0.1.70 → 0.1.72
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { AnnotationEngine } from '../engine/public/annotationEngine';
|
|
5
5
|
import useResizeObserver from './hooks/useResizeObserver';
|
|
@@ -8,24 +8,18 @@ import { useHotkeys } from './hooks/useHotkeys';
|
|
|
8
8
|
const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, drawing, events, enableHotkeys = true, editable = true, }) => {
|
|
9
9
|
const { panZoomEnabled, zoom, ZoomButton, resetOnImageChange = false } = options || {};
|
|
10
10
|
const { onImageLoadSuccess, onImageLoadError } = events || {};
|
|
11
|
-
const
|
|
12
|
-
const annotationsCanvasRef = useRef(null);
|
|
11
|
+
const containerRef = useRef(null);
|
|
13
12
|
const engineRef = useRef(null);
|
|
14
13
|
const pendingAnnotationsRef = useRef(null);
|
|
15
14
|
const imageLoadingRef = useRef(false);
|
|
16
15
|
const currentImageRef = useRef(image);
|
|
16
|
+
const engineIdRef = useRef(0);
|
|
17
17
|
const [, forceRender] = useState(0);
|
|
18
|
-
/* ---------- Resize ---------- */
|
|
18
|
+
/* ---------- Resize Observer ---------- */
|
|
19
19
|
useResizeObserver({
|
|
20
|
-
ref:
|
|
21
|
-
onResize: useDebounce((
|
|
22
|
-
|
|
23
|
-
const canvas = engine?.getImageCanvas();
|
|
24
|
-
if (!engine || !canvas)
|
|
25
|
-
return;
|
|
26
|
-
if (canvas.width !== size.width || canvas.height !== size.height) {
|
|
27
|
-
engine.initImageCanvas(true);
|
|
28
|
-
}
|
|
20
|
+
ref: containerRef,
|
|
21
|
+
onResize: useDebounce(() => {
|
|
22
|
+
engineRef.current?.initImageCanvas(true);
|
|
29
23
|
}, 150),
|
|
30
24
|
});
|
|
31
25
|
/* ---------- Hotkeys ---------- */
|
|
@@ -36,31 +30,45 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
|
|
|
36
30
|
enabled: enableHotkeys,
|
|
37
31
|
});
|
|
38
32
|
/* ---------- Image / Engine lifecycle ---------- */
|
|
39
|
-
const engineIdRef = useRef(0);
|
|
40
33
|
useEffect(() => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
engineRef.current?.destroy();
|
|
45
|
-
engineRef.current = null;
|
|
46
|
-
pendingAnnotationsRef.current = null;
|
|
47
|
-
currentImageRef.current = image;
|
|
48
|
-
engineIdRef.current++;
|
|
49
|
-
setAnnotations?.([]);
|
|
50
|
-
}
|
|
34
|
+
if (!containerRef.current)
|
|
35
|
+
return;
|
|
36
|
+
const prevEngine = engineRef.current;
|
|
51
37
|
imageLoadingRef.current = true;
|
|
52
38
|
if (!image?.trim()) {
|
|
53
|
-
|
|
39
|
+
if (prevEngine) {
|
|
40
|
+
prevEngine.destroy();
|
|
41
|
+
containerRef.current.removeChild(prevEngine.getImageCanvas());
|
|
42
|
+
containerRef.current.removeChild(prevEngine.getAnnotationsCanvas());
|
|
43
|
+
}
|
|
54
44
|
engineRef.current = null;
|
|
45
|
+
pendingAnnotationsRef.current = null;
|
|
55
46
|
imageLoadingRef.current = false;
|
|
47
|
+
currentImageRef.current = '';
|
|
56
48
|
return;
|
|
57
49
|
}
|
|
50
|
+
const newEngineId = engineIdRef.current + 1;
|
|
51
|
+
engineIdRef.current = newEngineId;
|
|
58
52
|
let cancelled = false;
|
|
59
53
|
const img = new Image();
|
|
60
54
|
img.onload = () => {
|
|
61
|
-
if (cancelled || !
|
|
55
|
+
if (cancelled || !containerRef.current)
|
|
62
56
|
return;
|
|
63
|
-
|
|
57
|
+
const imageCanvas = document.createElement('canvas');
|
|
58
|
+
const annotationsCanvas = document.createElement('canvas');
|
|
59
|
+
imageCanvas.style.position = 'absolute';
|
|
60
|
+
imageCanvas.style.width = '100%';
|
|
61
|
+
imageCanvas.style.height = '100%';
|
|
62
|
+
annotationsCanvas.style.position = 'absolute';
|
|
63
|
+
annotationsCanvas.style.width = '100%';
|
|
64
|
+
annotationsCanvas.style.height = '100%';
|
|
65
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
66
|
+
const dpr = window.devicePixelRatio || 1;
|
|
67
|
+
imageCanvas.width = containerRect.width * dpr;
|
|
68
|
+
imageCanvas.height = containerRect.height * dpr;
|
|
69
|
+
annotationsCanvas.width = containerRect.width * dpr;
|
|
70
|
+
annotationsCanvas.height = containerRect.height * dpr;
|
|
71
|
+
const prevImageCanvasState = prevEngine?.getImageCanvasState();
|
|
64
72
|
const imageCanvasState = !resetOnImageChange && prevImageCanvasState
|
|
65
73
|
? prevImageCanvasState
|
|
66
74
|
: {
|
|
@@ -75,52 +83,61 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
|
|
|
75
83
|
dh: img.height,
|
|
76
84
|
initZoom: 1,
|
|
77
85
|
};
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
engineRef.current = new AnnotationEngine({
|
|
82
|
-
imageCanvas: imageCanvasRef.current,
|
|
86
|
+
const engine = new AnnotationEngine({
|
|
87
|
+
imageCanvas,
|
|
88
|
+
annotationsCanvas,
|
|
83
89
|
image: img,
|
|
84
90
|
imageCanvasState,
|
|
85
|
-
|
|
86
|
-
annotations: initialAnnotations,
|
|
91
|
+
annotations: pendingAnnotationsRef.current ?? [],
|
|
87
92
|
setAnnotations: (next) => {
|
|
88
|
-
if (engineIdRef.current ===
|
|
93
|
+
if (engineIdRef.current === newEngineId)
|
|
89
94
|
setAnnotations?.(next);
|
|
90
|
-
}
|
|
91
95
|
},
|
|
92
96
|
drawing,
|
|
93
97
|
editable,
|
|
98
|
+
enableHotkeys,
|
|
94
99
|
panZoomEnabled,
|
|
95
100
|
zoom,
|
|
96
|
-
enableHotkeys,
|
|
97
101
|
onChange: () => forceRender((v) => v + 1),
|
|
98
102
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
requestAnimationFrame(() => {
|
|
104
|
+
containerRef.current.appendChild(imageCanvas);
|
|
105
|
+
containerRef.current.appendChild(annotationsCanvas);
|
|
106
|
+
engine.initImageCanvas(resetOnImageChange);
|
|
107
|
+
if (prevEngine) {
|
|
108
|
+
prevEngine.destroy();
|
|
109
|
+
containerRef.current.removeChild(prevEngine.getImageCanvas());
|
|
110
|
+
containerRef.current.removeChild(prevEngine.getAnnotationsCanvas());
|
|
111
|
+
}
|
|
112
|
+
engineRef.current = engine;
|
|
113
|
+
pendingAnnotationsRef.current = null;
|
|
114
|
+
imageLoadingRef.current = false;
|
|
115
|
+
currentImageRef.current = image;
|
|
116
|
+
onImageLoadSuccess?.();
|
|
117
|
+
});
|
|
102
118
|
};
|
|
103
119
|
img.onerror = () => {
|
|
104
|
-
if (
|
|
120
|
+
if (!containerRef.current)
|
|
105
121
|
return;
|
|
106
|
-
|
|
107
|
-
|
|
122
|
+
if (prevEngine) {
|
|
123
|
+
prevEngine.destroy();
|
|
124
|
+
containerRef.current.removeChild(prevEngine.getImageCanvas());
|
|
125
|
+
containerRef.current.removeChild(prevEngine.getAnnotationsCanvas());
|
|
126
|
+
}
|
|
108
127
|
engineRef.current = null;
|
|
128
|
+
pendingAnnotationsRef.current = null;
|
|
109
129
|
imageLoadingRef.current = false;
|
|
130
|
+
currentImageRef.current = '';
|
|
110
131
|
onImageLoadError?.(new Error(`Failed to load image: ${image}`));
|
|
111
132
|
};
|
|
112
133
|
img.src = image;
|
|
113
134
|
return () => {
|
|
114
135
|
cancelled = true;
|
|
115
|
-
img.onload = null;
|
|
116
|
-
img.onerror = null;
|
|
117
136
|
};
|
|
118
137
|
}, [image, resetOnImageChange]);
|
|
119
138
|
/* ---------- External annotations ---------- */
|
|
120
139
|
useEffect(() => {
|
|
121
|
-
if (!annotations)
|
|
122
|
-
return;
|
|
123
|
-
if (imageLoadingRef.current || !engineRef.current) {
|
|
140
|
+
if (!annotations || imageLoadingRef.current || !engineRef.current) {
|
|
124
141
|
pendingAnnotationsRef.current = annotations;
|
|
125
142
|
return;
|
|
126
143
|
}
|
|
@@ -131,20 +148,10 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
|
|
|
131
148
|
}
|
|
132
149
|
}, [annotations]);
|
|
133
150
|
/* ---------- Options sync ---------- */
|
|
134
|
-
useEffect(() =>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
useEffect(() => {
|
|
138
|
-
engineRef.current?.setEditable(editable);
|
|
139
|
-
}, [editable]);
|
|
140
|
-
useEffect(() => {
|
|
141
|
-
engineRef.current?.setDrawing(drawing);
|
|
142
|
-
}, [drawing]);
|
|
151
|
+
useEffect(() => engineRef.current?.setPanZoomEnabled(!!panZoomEnabled), [panZoomEnabled]);
|
|
152
|
+
useEffect(() => engineRef.current?.setEditable(editable), [editable]);
|
|
153
|
+
useEffect(() => engineRef.current?.setDrawing(drawing), [drawing]);
|
|
143
154
|
/* ---------- Render ---------- */
|
|
144
|
-
return (_jsx("div", { style: { width: '100%', height: '100%', display: 'flex', flex: 1 },
|
|
145
|
-
flex: 1,
|
|
146
|
-
position: 'relative',
|
|
147
|
-
cursor: !panZoomEnabled || editable ? 'default' : 'grab',
|
|
148
|
-
}, children: [_jsx("canvas", { ref: imageCanvasRef, style: { position: 'absolute', width: '100%', height: '100%' } }), _jsx("canvas", { ref: annotationsCanvasRef, style: { position: 'absolute', width: '100%', height: '100%' } }), ZoomButton && (_jsx(ZoomButton, { onClick: () => engineRef.current?.initImageCanvas(true), children: engineRef.current?.getZoomRatioLabel() }))] }) }));
|
|
155
|
+
return (_jsx("div", { ref: containerRef, style: { width: '100%', height: '100%', position: 'relative', display: 'flex', flex: 1 }, onWheel: (e) => engineRef.current?.onWheel(e.nativeEvent), onMouseDown: (e) => engineRef.current?.onMouseDown(e.nativeEvent), onMouseMove: (e) => engineRef.current?.onMouseMove(e.nativeEvent), onMouseUp: (e) => engineRef.current?.onMouseUp(e.nativeEvent), onMouseLeave: (e) => engineRef.current?.onMouseLeave(e.nativeEvent), onContextMenu: (e) => e.preventDefault(), children: ZoomButton && (_jsx(ZoomButton, { onClick: () => engineRef.current?.initImageCanvas(true), children: engineRef.current?.getZoomRatioLabel() })) }));
|
|
149
156
|
};
|
|
150
157
|
export default AnnotationCanvas;
|