@deepnoid/canvas 0.1.71 → 0.1.73

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