@deepnoid/canvas 0.1.18 → 0.1.20

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,11 +1,12 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useRef } from 'react';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
4
  import Canvas from './Canvas';
5
5
  import { useImageLoader } from '../hooks/useImageLoader';
6
6
  import { usePanZoom } from '../hooks/usePanZoom';
7
7
  const AnnotatedCanvas = ({ image, coordinates, panZoomable = false, ZoomButton, resetOnImageChange = true, editable = false, onImageLoadError, onImageLoadSuccess, }) => {
8
8
  const canvasRef = useRef(null);
9
+ const [displayCoordinates, setDisplayCoordinates] = useState();
9
10
  const { image: loadedImage, isLoading, error } = useImageLoader(image, 10000, onImageLoadSuccess, onImageLoadError);
10
11
  const { moveX, moveY, zoom, zoomX, zoomY, dx, dy, dw, dh, handleWheel, handleMouseDown, handleMouseMove, handleMouseUp, handleMouseLeave, resetView, } = usePanZoom({
11
12
  canvasRef,
@@ -13,13 +14,23 @@ const AnnotatedCanvas = ({ image, coordinates, panZoomable = false, ZoomButton,
13
14
  panZoomable,
14
15
  resetOnImageChange,
15
16
  });
17
+ useEffect(() => {
18
+ if (loadedImage && !isLoading) {
19
+ setDisplayCoordinates(coordinates);
20
+ }
21
+ }, [loadedImage, isLoading, coordinates]);
22
+ useEffect(() => {
23
+ if (isLoading) {
24
+ setDisplayCoordinates(undefined);
25
+ }
26
+ }, [isLoading]);
16
27
  if (!image || image.trim() === '' || error) {
17
28
  return null;
18
29
  }
19
- return (_jsx("div", { style: { width: '100%', height: '100%', display: 'flex', flex: 1 }, children: _jsxs("div", { onWheel: handleWheel, onMouseMove: handleMouseMove, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onMouseLeave: handleMouseLeave, style: {
30
+ return (_jsx("div", { style: { width: '100%', height: '100%', display: 'flex', flex: 1 }, children: _jsx("div", { onWheel: handleWheel, onMouseMove: handleMouseMove, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onMouseLeave: handleMouseLeave, style: {
20
31
  flex: 1,
21
32
  position: 'relative',
22
33
  cursor: panZoomable ? 'grab' : 'default',
23
- }, children: [_jsx("canvas", { ref: canvasRef, style: { position: 'absolute', width: '100%', height: '100%', left: 0, top: 0 } }), loadedImage && !isLoading && (_jsx(Canvas, { moveX: moveX, moveY: moveY, zoomX: zoomX, zoomY: zoomY, zoom: zoom, dx: dx, dy: dy, dw: dw, dh: dh, coordinates: coordinates, editable: editable })), ZoomButton && loadedImage && !isLoading && (_jsx(ZoomButton, { onClick: resetView, children: `${Math.round(zoom * 100)}%` }))] }) }));
34
+ }, children: loadedImage && !isLoading && (_jsxs(_Fragment, { children: [_jsx("canvas", { ref: canvasRef, style: { position: 'absolute', width: '100%', height: '100%', left: 0, top: 0 } }), _jsx(Canvas, { moveX: moveX, moveY: moveY, zoomX: zoomX, zoomY: zoomY, zoom: zoom, dx: dx, dy: dy, dw: dw, dh: dh, coordinates: displayCoordinates, editable: editable }), ZoomButton && _jsx(ZoomButton, { onClick: resetView, children: `${Math.round(zoom * 100)}%` })] })) }) }));
24
35
  };
25
36
  export default AnnotatedCanvas;
@@ -14,10 +14,38 @@ export const usePanZoom = ({ canvasRef, image, panZoomable = false, resetOnImage
14
14
  const [dh, setDh] = useState(0);
15
15
  const [status, setStatus] = useState();
16
16
  const [startMousePoint, setStartMousePoint] = useState();
17
+ const animationFrameRef = useRef(null);
18
+ const currentTransformRef = useRef({
19
+ moveX: 0,
20
+ moveY: 0,
21
+ zoom: 1,
22
+ zoomX: 0,
23
+ zoomY: 0,
24
+ dx: 0,
25
+ dy: 0,
26
+ });
17
27
  const ZOOM_UNIT = 0.9;
18
28
  const MAX_ZOOM = 4;
19
29
  const MIN_ZOOM = 0.5;
20
30
  const prevImageRef = useRef(null);
31
+ const immediateRender = useCallback((moveX, moveY, zoomX, zoomY, zoom, dx, dy) => {
32
+ const canvasEl = canvasRef.current;
33
+ if (!canvasEl || !image)
34
+ return;
35
+ const resolvedCanvas = resolutionCanvas(canvasEl);
36
+ if (!resolvedCanvas)
37
+ return;
38
+ drawCanvas(moveX, moveY, zoomX, zoomY, zoom, dx, dy, resolvedCanvas, image, true);
39
+ }, [canvasRef, image]);
40
+ const scheduleRender = useCallback(() => {
41
+ if (animationFrameRef.current) {
42
+ cancelAnimationFrame(animationFrameRef.current);
43
+ }
44
+ animationFrameRef.current = requestAnimationFrame(() => {
45
+ const current = currentTransformRef.current;
46
+ immediateRender(current.moveX, current.moveY, current.zoomX, current.zoomY, current.zoom, current.dx, current.dy);
47
+ });
48
+ }, [immediateRender]);
21
49
  const initCanvas = useCallback((img) => {
22
50
  const canvasEl = canvasRef.current;
23
51
  if (!canvasEl)
@@ -41,6 +69,15 @@ export const usePanZoom = ({ canvasRef, image, panZoomable = false, resetOnImage
41
69
  setZoomY(0);
42
70
  setMoveX(0);
43
71
  setMoveY(0);
72
+ currentTransformRef.current = {
73
+ moveX: 0,
74
+ moveY: 0,
75
+ zoom: scale,
76
+ zoomX: 0,
77
+ zoomY: 0,
78
+ dx,
79
+ dy,
80
+ };
44
81
  }, [canvasRef]);
45
82
  useEffect(() => {
46
83
  const canvasEl = canvasRef.current;
@@ -63,25 +100,36 @@ export const usePanZoom = ({ canvasRef, image, panZoomable = false, resetOnImage
63
100
  setDh(dh);
64
101
  setDx(dx);
65
102
  setDy(dy);
103
+ currentTransformRef.current.dx = dx;
104
+ currentTransformRef.current.dy = dy;
66
105
  }
67
106
  prevImageRef.current = image;
68
107
  }, [image, resetOnImageChange, initCanvas, canvasRef]);
69
108
  const handleWheel = useCallback((e) => {
70
- if (!panZoomable || !canvasRef.current || !image)
109
+ const canvasEl = canvasRef.current;
110
+ if (!panZoomable || !canvasRef.current || !canvasEl || !image)
71
111
  return;
72
- let nextZoom = e.deltaY < 0 ? zoom / ZOOM_UNIT : zoom * ZOOM_UNIT;
112
+ const currentZoom = currentTransformRef.current.zoom;
113
+ let nextZoom = e.deltaY < 0 ? currentZoom / ZOOM_UNIT : currentZoom * ZOOM_UNIT;
73
114
  nextZoom = Math.min(Math.max(nextZoom, MIN_ZOOM), MAX_ZOOM);
74
- if ((zoom <= MIN_ZOOM && e.deltaY > 0) || (zoom >= MAX_ZOOM && e.deltaY < 0))
115
+ if ((currentZoom <= MIN_ZOOM && e.deltaY > 0) || (currentZoom >= MAX_ZOOM && e.deltaY < 0))
75
116
  return;
76
- const canvasEl = resolutionCanvas(canvasRef.current);
77
- const zoomPoint = {
78
- x: -dx - moveX + canvasEl.width / 2,
79
- y: -dy - moveY + canvasEl.height / 2,
80
- };
81
- setZoomX(zoomPoint.x);
82
- setZoomY(zoomPoint.y);
117
+ const resolvedCanvas = resolutionCanvas(canvasEl);
118
+ if (!resolvedCanvas)
119
+ return;
120
+ const resolution_ratio_x = resolvedCanvas.width / canvasEl.clientWidth;
121
+ const resolution_ratio_y = resolvedCanvas.height / canvasEl.clientHeight;
122
+ const current = currentTransformRef.current;
123
+ const centerX = resolvedCanvas.width / 2 - current.dx * resolution_ratio_x - current.moveX * resolution_ratio_x;
124
+ const centerY = resolvedCanvas.height / 2 - current.dy * resolution_ratio_y - current.moveY * resolution_ratio_y;
125
+ currentTransformRef.current.zoom = nextZoom;
126
+ currentTransformRef.current.zoomX = centerX;
127
+ currentTransformRef.current.zoomY = centerY;
128
+ immediateRender(current.moveX, current.moveY, centerX, centerY, nextZoom, current.dx, current.dy);
129
+ setZoomX(centerX);
130
+ setZoomY(centerY);
83
131
  setZoom(nextZoom);
84
- }, [canvasRef, panZoomable, zoom, dx, dy, moveX, moveY, image]);
132
+ }, [canvasRef, panZoomable, image, immediateRender]);
85
133
  const handleMouseDown = useCallback((e) => {
86
134
  if (!panZoomable || !canvasRef.current)
87
135
  return;
@@ -89,25 +137,32 @@ export const usePanZoom = ({ canvasRef, image, panZoomable = false, resetOnImage
89
137
  const rect = canvasRef.current.getBoundingClientRect();
90
138
  const mouseX = e.clientX - rect.left;
91
139
  const mouseY = e.clientY - rect.top;
92
- const transformed = getMousePointTransform({ x: mouseX, y: mouseY }, { x: moveX, y: moveY }, { x: zoomX, y: zoomY }, { x: dx, y: dy }, zoom, canvasRef.current);
140
+ const current = currentTransformRef.current;
141
+ const transformed = getMousePointTransform({ x: mouseX, y: mouseY }, { x: current.moveX, y: current.moveY }, { x: current.zoomX, y: current.zoomY }, { x: current.dx, y: current.dy }, current.zoom, canvasRef.current);
93
142
  setStartMousePoint({ x: transformed.x, y: transformed.y });
94
143
  e.preventDefault();
95
- }, [canvasRef, panZoomable, moveX, moveY, zoomX, zoomY, dx, dy, zoom]);
144
+ }, [canvasRef, panZoomable]);
96
145
  const handleMouseMove = useCallback((e) => {
97
146
  if (!panZoomable || status !== MouseStatus.MOVE || !canvasRef.current)
98
147
  return;
99
148
  const rect = canvasRef.current.getBoundingClientRect();
100
149
  const mouseX = e.clientX - rect.left;
101
150
  const mouseY = e.clientY - rect.top;
102
- const transformed = getMousePointTransform({ x: mouseX, y: mouseY }, { x: moveX, y: moveY }, { x: zoomX, y: zoomY }, { x: dx, y: dy }, zoom, canvasRef.current);
151
+ const current = currentTransformRef.current;
152
+ const transformed = getMousePointTransform({ x: mouseX, y: mouseY }, { x: current.moveX, y: current.moveY }, { x: current.zoomX, y: current.zoomY }, { x: current.dx, y: current.dy }, current.zoom, canvasRef.current);
103
153
  if (startMousePoint) {
104
154
  const deltaX = transformed.x - startMousePoint.x;
105
155
  const deltaY = transformed.y - startMousePoint.y;
106
- setMoveX((prev) => prev + deltaX);
107
- setMoveY((prev) => prev + deltaY);
156
+ const newMoveX = current.moveX + deltaX;
157
+ const newMoveY = current.moveY + deltaY;
158
+ currentTransformRef.current.moveX = newMoveX;
159
+ currentTransformRef.current.moveY = newMoveY;
160
+ immediateRender(newMoveX, newMoveY, current.zoomX, current.zoomY, current.zoom, current.dx, current.dy);
161
+ setMoveX(newMoveX);
162
+ setMoveY(newMoveY);
108
163
  }
109
164
  e.preventDefault();
110
- }, [canvasRef, panZoomable, status, moveX, moveY, zoomX, zoomY, dx, dy, zoom, startMousePoint]);
165
+ }, [canvasRef, panZoomable, status, startMousePoint, immediateRender]);
111
166
  const handleMouseUp = useCallback(() => {
112
167
  if (!panZoomable)
113
168
  return;
@@ -121,12 +176,32 @@ export const usePanZoom = ({ canvasRef, image, panZoomable = false, resetOnImage
121
176
  useEffect(() => {
122
177
  if (!canvasRef.current || !image)
123
178
  return;
124
- drawCanvas(moveX, moveY, zoomX, zoomY, zoom, dx, dy, resolutionCanvas(canvasRef.current), image, true);
125
- }, [canvasRef, image, moveX, moveY, zoomX, zoomY, zoom, dx, dy]);
179
+ if (dx !== 0 || dy !== 0 || zoom !== 1) {
180
+ scheduleRender();
181
+ }
182
+ }, [canvasRef, image, moveX, moveY, zoomX, zoomY, zoom, dx, dy, scheduleRender]);
183
+ useEffect(() => {
184
+ currentTransformRef.current = {
185
+ moveX,
186
+ moveY,
187
+ zoom,
188
+ zoomX,
189
+ zoomY,
190
+ dx,
191
+ dy,
192
+ };
193
+ }, [moveX, moveY, zoom, zoomX, zoomY, dx, dy]);
126
194
  const resetView = useCallback(() => {
127
195
  if (image)
128
196
  initCanvas(image);
129
197
  }, [image, initCanvas]);
198
+ useEffect(() => {
199
+ return () => {
200
+ if (animationFrameRef.current) {
201
+ cancelAnimationFrame(animationFrameRef.current);
202
+ }
203
+ };
204
+ }, []);
130
205
  return {
131
206
  moveX,
132
207
  moveY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepnoid/canvas",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",