@deepnoid/canvas 0.1.20 → 0.1.21

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.
@@ -8,29 +8,29 @@ const AnnotatedCanvas = ({ image, coordinates, panZoomable = false, ZoomButton,
8
8
  const canvasRef = useRef(null);
9
9
  const [displayCoordinates, setDisplayCoordinates] = useState();
10
10
  const { image: loadedImage, isLoading, error } = useImageLoader(image, 10000, onImageLoadSuccess, onImageLoadError);
11
- const { moveX, moveY, zoom, zoomX, zoomY, dx, dy, dw, dh, handleWheel, handleMouseDown, handleMouseMove, handleMouseUp, handleMouseLeave, resetView, } = usePanZoom({
11
+ const { moveX, moveY, zoom, initZoom, zoomX, zoomY, dx, dy, dw, dh, handleWheel, handleMouseDown, handleMouseMove, handleMouseUp, handleMouseLeave, initCanvas, } = usePanZoom({
12
12
  canvasRef,
13
13
  image: loadedImage,
14
14
  panZoomable,
15
15
  resetOnImageChange,
16
16
  });
17
+ const loaded = loadedImage && !isLoading;
17
18
  useEffect(() => {
18
- if (loadedImage && !isLoading) {
19
+ if (loaded) {
19
20
  setDisplayCoordinates(coordinates);
20
21
  }
21
- }, [loadedImage, isLoading, coordinates]);
22
+ }, [loaded, coordinates]);
22
23
  useEffect(() => {
23
24
  if (isLoading) {
24
25
  setDisplayCoordinates(undefined);
25
26
  }
26
27
  }, [isLoading]);
27
- if (!image || image.trim() === '' || error) {
28
+ if (!image || error)
28
29
  return null;
29
- }
30
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: {
31
31
  flex: 1,
32
32
  position: 'relative',
33
33
  cursor: panZoomable ? 'grab' : 'default',
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)}%` })] })) }) }));
34
+ }, children: loaded && (_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 && initZoom > 0 && (_jsx(ZoomButton, { onClick: initCanvas, children: `${Math.round((zoom / initZoom) * 100)}%` }))] })) }) }));
35
35
  };
36
36
  export default AnnotatedCanvas;
@@ -9,17 +9,18 @@ export declare const usePanZoom: ({ canvasRef, image, panZoomable, resetOnImageC
9
9
  moveX: number;
10
10
  moveY: number;
11
11
  zoom: number;
12
+ initZoom: number;
12
13
  zoomX: number;
13
14
  zoomY: number;
14
15
  dx: number;
15
16
  dy: number;
16
17
  dw: number;
17
18
  dh: number;
18
- handleWheel: (e: WheelEvent) => void;
19
- handleMouseDown: (e: MouseEvent) => void;
20
- handleMouseMove: (e: MouseEvent) => void;
21
- handleMouseUp: () => void;
22
- handleMouseLeave: () => void;
23
- resetView: () => void;
19
+ handleWheel: (event: WheelEvent) => void;
20
+ handleMouseDown: (event: MouseEvent) => false | undefined;
21
+ handleMouseMove: (event: MouseEvent) => void;
22
+ handleMouseUp: (event: MouseEvent) => void;
23
+ handleMouseLeave: (event: MouseEvent) => void;
24
+ initCanvas: () => void;
24
25
  };
25
26
  export {};
@@ -1,211 +1,179 @@
1
1
  'use client';
2
2
  import { useCallback, useEffect, useState, useRef } from 'react';
3
- import { resolutionCanvas, drawCanvas, getMousePointTransform } from '../utils/graphic';
3
+ import { resolutionCanvas, drawCanvas, getMousePointTransform, canvasCenterPoint } from '../utils/graphic';
4
4
  import { MouseStatus } from '../enum/common';
5
+ import useResizeObserver from './useResizeObserver';
5
6
  export const usePanZoom = ({ canvasRef, image, panZoomable = false, resetOnImageChange = true }) => {
6
- const [moveX, setMoveX] = useState(0);
7
- const [moveY, setMoveY] = useState(0);
8
- const [zoom, setZoom] = useState(1);
7
+ const ZOOM_UNIT = 0.9;
8
+ const MAX_ZOOM = 4;
9
+ const MIN_ZOOM = 0.5;
10
+ const { width, height } = useResizeObserver({ ref: canvasRef });
11
+ const [initZoom, setInitZoom] = useState(0);
12
+ const [zoom, setZoom] = useState(0);
9
13
  const [zoomX, setZoomX] = useState(0);
10
14
  const [zoomY, setZoomY] = useState(0);
11
15
  const [dx, setDx] = useState(0);
12
16
  const [dy, setDy] = useState(0);
13
17
  const [dw, setDw] = useState(0);
14
18
  const [dh, setDh] = useState(0);
15
- const [status, setStatus] = useState();
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
- });
27
- const ZOOM_UNIT = 0.9;
28
- const MAX_ZOOM = 4;
29
- const MIN_ZOOM = 0.5;
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);
19
+ const [moveX, setMoveX] = useState(0);
20
+ const [moveY, setMoveY] = useState(0);
21
+ const [status, setStatus] = useState('');
22
+ const [imageOnloadCount, setImageOnloadCount] = useState(0);
23
+ const [_imageInfo, _setImageInfo] = useState();
24
+ const imageRef = useRef(new Image());
25
+ const calculatorZoomPoint = useCallback((canvasEl, movementX, movementY, dx, dy) => {
26
+ let x = 0;
27
+ let y = 0;
28
+ if (canvasRef.current) {
29
+ x = -dx - movementX + canvasEl.width / 2;
30
+ y = -dy - movementY + canvasEl.height / 2;
43
31
  }
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]);
49
- const initCanvas = useCallback((img) => {
50
- const canvasEl = canvasRef.current;
51
- if (!canvasEl)
52
- return;
53
- const canvasWidth = canvasEl.clientWidth;
54
- const canvasHeight = canvasEl.clientHeight;
55
- let scale = 1;
56
- if (img.width > canvasWidth || img.height > canvasHeight) {
57
- scale = Math.min(canvasWidth / img.width, canvasHeight / img.height);
32
+ return { x, y };
33
+ }, []);
34
+ const calculateInitZoom = useCallback((image, canvasEl) => {
35
+ if (canvasEl.clientWidth < canvasEl.clientHeight) {
36
+ return canvasEl.clientWidth / canvasEl.clientHeight < image.width / image.height
37
+ ? canvasEl.clientWidth / image.width
38
+ : canvasEl.clientHeight / image.height;
39
+ }
40
+ else {
41
+ return canvasEl.clientWidth / canvasEl.clientHeight > image.width / image.height
42
+ ? canvasEl.clientHeight / image.height
43
+ : canvasEl.clientWidth / image.width;
58
44
  }
59
- const dw = img.width * scale;
60
- const dh = img.height * scale;
61
- const dx = (canvasWidth - dw) / 2;
62
- const dy = (canvasHeight - dh) / 2;
63
- setDx(dx);
64
- setDy(dy);
65
- setDw(dw);
66
- setDh(dh);
67
- setZoom(scale);
68
- setZoomX(0);
69
- setZoomY(0);
45
+ }, []);
46
+ const init = (canvasEl, image) => {
47
+ const point = canvasCenterPoint(canvasEl, image);
48
+ setDx(point.x);
49
+ setDy(point.y);
50
+ setDw(image.width);
51
+ setDh(image.height);
70
52
  setMoveX(0);
71
53
  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
- };
81
- }, [canvasRef]);
54
+ const init_zoom = calculateInitZoom(image, canvasEl);
55
+ setZoom(init_zoom);
56
+ setInitZoom(init_zoom);
57
+ const zoomPoint = calculatorZoomPoint(canvasEl, 0, 0, point.x, point.y);
58
+ setZoomX(zoomPoint.x);
59
+ setZoomY(zoomPoint.y);
60
+ };
61
+ const initCanvas = () => {
62
+ if (image) {
63
+ const canvasEl = resolutionCanvas(canvasRef.current);
64
+ if (canvasEl)
65
+ init(canvasEl, imageRef.current);
66
+ setImageOnloadCount((prev) => prev + 1);
67
+ }
68
+ };
82
69
  useEffect(() => {
83
- const canvasEl = canvasRef.current;
84
- if (!canvasEl || !image)
85
- return;
86
- const canvasWidth = canvasEl.clientWidth;
87
- const canvasHeight = canvasEl.clientHeight;
88
- const isTooBig = image.width > canvasWidth || image.height > canvasHeight;
89
- const isFirstLoad = !prevImageRef.current;
90
- if (resetOnImageChange || isFirstLoad) {
91
- initCanvas(image);
70
+ if (image) {
71
+ imageRef.current.src = image.src;
72
+ imageRef.current.onload = () => {
73
+ if (resetOnImageChange) {
74
+ const canvasEl = resolutionCanvas(canvasRef.current);
75
+ if (canvasEl)
76
+ init(canvasEl, imageRef.current);
77
+ }
78
+ setImageOnloadCount((prev) => prev + 1);
79
+ };
92
80
  }
93
- else if (isTooBig) {
94
- const scale = Math.min(canvasWidth / image.width, canvasHeight / image.height);
95
- const dw = image.width * scale;
96
- const dh = image.height * scale;
97
- const dx = (canvasWidth - dw) / 2;
98
- const dy = (canvasHeight - dh) / 2;
99
- setDw(dw);
100
- setDh(dh);
101
- setDx(dx);
102
- setDy(dy);
103
- currentTransformRef.current.dx = dx;
104
- currentTransformRef.current.dy = dy;
81
+ }, [image]);
82
+ const [startMousePoint, setStartMousePoint] = useState();
83
+ useEffect(() => {
84
+ const redraw = () => {
85
+ if (canvasRef.current) {
86
+ drawCanvas(moveX, moveY, zoomX, zoomY, zoom, dx, dy, resolutionCanvas(canvasRef.current), imageRef.current, true);
87
+ }
88
+ };
89
+ redraw();
90
+ }, [moveX, moveY, zoomX, zoomY, zoom, dx, dy, dw, dh, imageOnloadCount]);
91
+ useEffect(() => {
92
+ if (image) {
93
+ imageRef.current.src = image.src;
94
+ const canvasEl = resolutionCanvas(canvasRef.current);
95
+ if (canvasEl)
96
+ init(canvasEl, imageRef.current);
97
+ setImageOnloadCount((prev) => prev + 1);
105
98
  }
106
- prevImageRef.current = image;
107
- }, [image, resetOnImageChange, initCanvas, canvasRef]);
108
- const handleWheel = useCallback((e) => {
109
- const canvasEl = canvasRef.current;
110
- if (!panZoomable || !canvasRef.current || !canvasEl || !image)
111
- return;
112
- const currentZoom = currentTransformRef.current.zoom;
113
- let nextZoom = e.deltaY < 0 ? currentZoom / ZOOM_UNIT : currentZoom * ZOOM_UNIT;
114
- nextZoom = Math.min(Math.max(nextZoom, MIN_ZOOM), MAX_ZOOM);
115
- if ((currentZoom <= MIN_ZOOM && e.deltaY > 0) || (currentZoom >= MAX_ZOOM && e.deltaY < 0))
116
- return;
117
- const resolvedCanvas = resolutionCanvas(canvasEl);
118
- if (!resolvedCanvas)
99
+ }, [width, height]);
100
+ const handleWheel = (event) => {
101
+ if (!panZoomable)
119
102
  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);
131
- setZoom(nextZoom);
132
- }, [canvasRef, panZoomable, image, immediateRender]);
133
- const handleMouseDown = useCallback((e) => {
134
- if (!panZoomable || !canvasRef.current)
103
+ if (canvasRef.current) {
104
+ let calc_zoom = event.deltaY < 0 ? (zoom || 1) * (1 / ZOOM_UNIT) : (zoom || 1) * ZOOM_UNIT;
105
+ if (initZoom * MAX_ZOOM < zoom * ZOOM_UNIT)
106
+ calc_zoom = calc_zoom * ZOOM_UNIT;
107
+ if (initZoom * MIN_ZOOM > zoom * ZOOM_UNIT)
108
+ calc_zoom = calc_zoom * (1 / ZOOM_UNIT);
109
+ const canvasEl = canvasRef.current;
110
+ const zoomPoint = calculatorZoomPoint(canvasEl, moveX, moveY, dx, dy);
111
+ setZoomX(zoomPoint.x);
112
+ setZoomY(zoomPoint.y);
113
+ setZoom(calc_zoom);
114
+ }
115
+ };
116
+ const handleMouseMove = (event) => {
117
+ if (!panZoomable)
135
118
  return;
119
+ if (status === MouseStatus.MOVE) {
120
+ if (canvasRef.current) {
121
+ const canvasEl = canvasRef.current;
122
+ const rect = canvasEl.getBoundingClientRect();
123
+ const mouseX = event.clientX - rect.left;
124
+ const mouseY = event.clientY - rect.top;
125
+ const mouse = getMousePointTransform({ x: mouseX, y: mouseY }, { x: moveX, y: moveY }, { x: zoomX, y: zoomY }, { x: dx, y: dy }, zoom || 1, canvasEl);
126
+ let x = mouse.x;
127
+ let y = mouse.y;
128
+ if (startMousePoint) {
129
+ x = x - startMousePoint.x;
130
+ y = y - startMousePoint.y;
131
+ }
132
+ setMoveX(moveX + x);
133
+ setMoveY(moveY + y);
134
+ const zoomPoint = calculatorZoomPoint(canvasEl, moveX + x, moveY + y, dx, dy);
135
+ setZoomX(zoomPoint.x);
136
+ setZoomY(zoomPoint.y);
137
+ }
138
+ }
139
+ event.preventDefault();
140
+ };
141
+ const handleMouseDown = (event) => {
142
+ if (!panZoomable)
143
+ return false;
136
144
  setStatus(MouseStatus.MOVE);
137
- const rect = canvasRef.current.getBoundingClientRect();
138
- const mouseX = e.clientX - rect.left;
139
- const mouseY = e.clientY - rect.top;
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);
142
- setStartMousePoint({ x: transformed.x, y: transformed.y });
143
- e.preventDefault();
144
- }, [canvasRef, panZoomable]);
145
- const handleMouseMove = useCallback((e) => {
146
- if (!panZoomable || status !== MouseStatus.MOVE || !canvasRef.current)
147
- return;
148
- const rect = canvasRef.current.getBoundingClientRect();
149
- const mouseX = e.clientX - rect.left;
150
- const mouseY = e.clientY - rect.top;
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);
153
- if (startMousePoint) {
154
- const deltaX = transformed.x - startMousePoint.x;
155
- const deltaY = transformed.y - startMousePoint.y;
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);
145
+ if (canvasRef.current) {
146
+ const canvasEl = canvasRef.current;
147
+ const rect = canvasEl.getBoundingClientRect();
148
+ const mouseX = event.clientX - rect.left;
149
+ const mouseY = event.clientY - rect.top;
150
+ const mouse = getMousePointTransform({ x: mouseX, y: mouseY }, { x: moveX, y: moveY }, { x: zoomX, y: zoomY }, { x: dx, y: dy }, zoom || 1, canvasEl);
151
+ const x = mouse.x;
152
+ const y = mouse.y;
153
+ setStartMousePoint({
154
+ x: x,
155
+ y: y,
156
+ });
163
157
  }
164
- e.preventDefault();
165
- }, [canvasRef, panZoomable, status, startMousePoint, immediateRender]);
166
- const handleMouseUp = useCallback(() => {
158
+ event.preventDefault();
159
+ };
160
+ const handleMouseUp = (event) => {
167
161
  if (!panZoomable)
168
162
  return;
169
163
  setStatus(MouseStatus.STOP);
170
- }, [panZoomable]);
171
- const handleMouseLeave = useCallback(() => {
164
+ event.preventDefault();
165
+ };
166
+ const handleMouseLeave = (event) => {
172
167
  if (!panZoomable)
173
168
  return;
174
169
  setStatus(MouseStatus.STOP);
175
- }, [panZoomable]);
176
- useEffect(() => {
177
- if (!canvasRef.current || !image)
178
- return;
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]);
194
- const resetView = useCallback(() => {
195
- if (image)
196
- initCanvas(image);
197
- }, [image, initCanvas]);
198
- useEffect(() => {
199
- return () => {
200
- if (animationFrameRef.current) {
201
- cancelAnimationFrame(animationFrameRef.current);
202
- }
203
- };
204
- }, []);
170
+ event.preventDefault();
171
+ };
205
172
  return {
206
173
  moveX,
207
174
  moveY,
208
175
  zoom,
176
+ initZoom,
209
177
  zoomX,
210
178
  zoomY,
211
179
  dx,
@@ -217,6 +185,6 @@ export const usePanZoom = ({ canvasRef, image, panZoomable = false, resetOnImage
217
185
  handleMouseMove,
218
186
  handleMouseUp,
219
187
  handleMouseLeave,
220
- resetView,
188
+ initCanvas,
221
189
  };
222
190
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepnoid/canvas",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",