@deepnoid/canvas 0.1.71 → 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, 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';
@@ -8,25 +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 imageCanvasRef = useRef(null);
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);
17
16
  const engineIdRef = useRef(0);
18
17
  const [, forceRender] = useState(0);
19
- /* ---------- Resize ---------- */
18
+ /* ---------- Resize Observer ---------- */
20
19
  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
- }
20
+ ref: containerRef,
21
+ onResize: useDebounce(() => {
22
+ engineRef.current?.initImageCanvas(true);
30
23
  }, 150),
31
24
  });
32
25
  /* ---------- Hotkeys ---------- */
@@ -38,28 +31,44 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
38
31
  });
39
32
  /* ---------- Image / Engine lifecycle ---------- */
40
33
  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
- }
34
+ if (!containerRef.current)
35
+ return;
36
+ const prevEngine = engineRef.current;
51
37
  imageLoadingRef.current = true;
52
38
  if (!image?.trim()) {
53
- engineRef.current?.destroy();
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 || !imageCanvasRef.current || !annotationsCanvasRef.current)
55
+ if (cancelled || !containerRef.current)
62
56
  return;
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();
63
72
  const imageCanvasState = !resetOnImageChange && prevImageCanvasState
64
73
  ? prevImageCanvasState
65
74
  : {
@@ -74,52 +83,61 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
74
83
  dh: img.height,
75
84
  initZoom: 1,
76
85
  };
77
- const initialAnnotations = pendingAnnotationsRef.current ?? [];
78
- const currentEngineId = engineIdRef.current;
79
- pendingAnnotationsRef.current = null;
80
- engineRef.current = new AnnotationEngine({
81
- imageCanvas: imageCanvasRef.current,
86
+ const engine = new AnnotationEngine({
87
+ imageCanvas,
88
+ annotationsCanvas,
82
89
  image: img,
83
90
  imageCanvasState,
84
- annotationsCanvas: annotationsCanvasRef.current,
85
- annotations: initialAnnotations,
91
+ annotations: pendingAnnotationsRef.current ?? [],
86
92
  setAnnotations: (next) => {
87
- if (engineIdRef.current === currentEngineId) {
93
+ if (engineIdRef.current === newEngineId)
88
94
  setAnnotations?.(next);
89
- }
90
95
  },
91
96
  drawing,
92
97
  editable,
98
+ enableHotkeys,
93
99
  panZoomEnabled,
94
100
  zoom,
95
- enableHotkeys,
96
101
  onChange: () => forceRender((v) => v + 1),
97
102
  });
98
- engineRef.current.initImageCanvas(resetOnImageChange);
99
- imageLoadingRef.current = false;
100
- onImageLoadSuccess?.();
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
+ });
101
118
  };
102
119
  img.onerror = () => {
103
- if (cancelled)
120
+ if (!containerRef.current)
104
121
  return;
105
- pendingAnnotationsRef.current = null;
106
- engineRef.current?.destroy();
122
+ if (prevEngine) {
123
+ prevEngine.destroy();
124
+ containerRef.current.removeChild(prevEngine.getImageCanvas());
125
+ containerRef.current.removeChild(prevEngine.getAnnotationsCanvas());
126
+ }
107
127
  engineRef.current = null;
128
+ pendingAnnotationsRef.current = null;
108
129
  imageLoadingRef.current = false;
130
+ currentImageRef.current = '';
109
131
  onImageLoadError?.(new Error(`Failed to load image: ${image}`));
110
132
  };
111
133
  img.src = image;
112
134
  return () => {
113
135
  cancelled = true;
114
- img.onload = null;
115
- img.onerror = null;
116
136
  };
117
137
  }, [image, resetOnImageChange]);
118
138
  /* ---------- External annotations ---------- */
119
139
  useEffect(() => {
120
- if (!annotations)
121
- return;
122
- if (imageLoadingRef.current || !engineRef.current) {
140
+ if (!annotations || imageLoadingRef.current || !engineRef.current) {
123
141
  pendingAnnotationsRef.current = annotations;
124
142
  return;
125
143
  }
@@ -130,20 +148,10 @@ const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, dr
130
148
  }
131
149
  }, [annotations]);
132
150
  /* ---------- 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]);
151
+ useEffect(() => engineRef.current?.setPanZoomEnabled(!!panZoomEnabled), [panZoomEnabled]);
152
+ useEffect(() => engineRef.current?.setEditable(editable), [editable]);
153
+ useEffect(() => engineRef.current?.setDrawing(drawing), [drawing]);
142
154
  /* ---------- 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() }))] }) }));
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() })) }));
148
156
  };
149
157
  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.72",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",