@deepnoid/canvas 0.1.2 → 0.1.3

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.
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { drawCanvas, drawRect, resolutionCanvas } from '../utils/graphic';
4
+ const Canvas = ({ moveX, moveY, zoom, zoomX, zoomY, dx, dy, dw, dh, coordinates }) => {
5
+ const canvas = useRef(null);
6
+ useEffect(() => {
7
+ const _redraw = () => {
8
+ const canvas_el = resolutionCanvas(canvas.current);
9
+ if (canvas_el) {
10
+ drawCanvas(moveX, moveY, zoomX, zoomY, zoom, dx, dy, canvas_el, undefined, true);
11
+ const context = canvas_el.getContext('2d');
12
+ if (context && coordinates) {
13
+ const deep_copy = JSON.parse(JSON.stringify(coordinates));
14
+ deep_copy.forEach(coordinate => {
15
+ drawRect(context, coordinate);
16
+ });
17
+ }
18
+ }
19
+ };
20
+ _redraw();
21
+ window.addEventListener('resize', _redraw);
22
+ return () => {
23
+ window.removeEventListener('resize', _redraw);
24
+ };
25
+ }, [moveX, moveY, zoomX, zoomY, zoom, dx, dy, dw, dh, coordinates]);
26
+ return (_jsx("canvas", { ref: canvas, style: {
27
+ position: 'absolute',
28
+ height: '100%',
29
+ width: '100%',
30
+ backgroundColor: 'transparent',
31
+ transition: 'all 500ms'
32
+ } }));
33
+ };
34
+ export default Canvas;
@@ -0,0 +1,192 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import useResizeObserver from '../hooks/useResizeObserver';
4
+ import { canvasCenterPoint, drawCanvas, getMousePointTransform, resolutionCanvas } from '../utils/graphic';
5
+ import Canvas from './Canvas';
6
+ const XrayViewer = ({ image, hasZoom, coordinates }) => {
7
+ const wrapper = useRef(null);
8
+ const viewer = useRef(null);
9
+ const { width, height } = useResizeObserver({ ref: viewer });
10
+ const ZOOM_UNIT = 0.9;
11
+ const MAX_ZOOM = 4;
12
+ const MIN_ZOOM = 0.5;
13
+ const [initZoom, setInitZoom] = useState(0);
14
+ const [zoom, setZoom] = useState(0);
15
+ const [zoomX, setZoomX] = useState(0);
16
+ const [zoomY, setZoomY] = useState(0);
17
+ const [dx, setDx] = useState(0);
18
+ const [dy, setDy] = useState(0);
19
+ const [dw, setDw] = useState(0);
20
+ const [dh, setDh] = useState(0);
21
+ const [moveX, setMoveX] = useState(0);
22
+ const [moveY, setMoveY] = useState(0);
23
+ const [status, setStatus] = useState('');
24
+ const [imageOnloadCount, setImageOnloadCount] = useState(0);
25
+ const [_imageInfo, _setImageInfo] = useState();
26
+ const imageRef = useRef(new Image());
27
+ const prevImageInfo = useRef(undefined);
28
+ const calculatorZoomPoint = useCallback((canvas_el, movementX, movementY, dx, dy) => {
29
+ let x = 0;
30
+ let y = 0;
31
+ if (viewer.current) {
32
+ x = -dx - movementX + canvas_el.width / 2;
33
+ y = -dy - movementY + canvas_el.height / 2;
34
+ }
35
+ return { x, y };
36
+ }, []);
37
+ const calculateInitZoom = useCallback((image, canvas_el) => {
38
+ if (canvas_el.clientWidth < canvas_el.clientHeight) {
39
+ return canvas_el.clientWidth / canvas_el.clientHeight < image.width / image.height
40
+ ? canvas_el.clientWidth / image.width
41
+ : canvas_el.clientHeight / image.height;
42
+ }
43
+ else {
44
+ return canvas_el.clientWidth / canvas_el.clientHeight > image.width / image.height
45
+ ? canvas_el.clientHeight / image.height
46
+ : canvas_el.clientWidth / image.width;
47
+ }
48
+ }, []);
49
+ const init = (canvas_el, image) => {
50
+ const point = canvasCenterPoint(canvas_el, image);
51
+ setDx(point.x);
52
+ setDy(point.y);
53
+ setDw(image.width);
54
+ setDh(image.height);
55
+ setMoveX(0);
56
+ setMoveY(0);
57
+ const init_zoom = calculateInitZoom(image, canvas_el);
58
+ setZoom(init_zoom);
59
+ setInitZoom(init_zoom);
60
+ const zoomPoint = calculatorZoomPoint(canvas_el, 0, 0, point.x, point.y);
61
+ setZoomX(zoomPoint.x);
62
+ setZoomY(zoomPoint.y);
63
+ };
64
+ const initCanvas = () => {
65
+ if (image) {
66
+ const canvas_el = resolutionCanvas(viewer.current);
67
+ if (canvas_el)
68
+ init(canvas_el, imageRef.current);
69
+ setImageOnloadCount(prev => prev + 1);
70
+ }
71
+ };
72
+ useEffect(() => {
73
+ if (image) {
74
+ imageRef.current.src = image.imagePath;
75
+ const imageInfo = `${image.itemId}-${image.type}`;
76
+ imageRef.current.onload = () => {
77
+ if (prevImageInfo.current !== imageInfo || ['REALITY'].includes(image.type)) {
78
+ prevImageInfo.current = imageInfo;
79
+ const canvas_el = resolutionCanvas(viewer.current);
80
+ if (canvas_el)
81
+ init(canvas_el, imageRef.current);
82
+ }
83
+ setImageOnloadCount(prev => prev + 1);
84
+ };
85
+ }
86
+ }, [image]);
87
+ const [startMousePoint, setStartMousePoint] = useState();
88
+ useEffect(() => {
89
+ const _redraw = () => {
90
+ if (viewer.current) {
91
+ drawCanvas(moveX, moveY, zoomX, zoomY, zoom, dx, dy, resolutionCanvas(viewer.current), imageRef.current, true);
92
+ }
93
+ };
94
+ _redraw();
95
+ }, [moveX, moveY, zoomX, zoomY, zoom, dx, dy, dw, dh, imageOnloadCount]);
96
+ useEffect(() => {
97
+ if (image) {
98
+ imageRef.current.src = image.imagePath;
99
+ const imageInfo = `${image.itemId}-${image.type}`;
100
+ if (prevImageInfo.current === imageInfo) {
101
+ const canvas_el = resolutionCanvas(viewer.current);
102
+ if (canvas_el)
103
+ init(canvas_el, imageRef.current);
104
+ setImageOnloadCount(prev => prev + 1);
105
+ }
106
+ }
107
+ }, [width, height]);
108
+ const handleWheel = (event) => {
109
+ if (!hasZoom)
110
+ return;
111
+ if (viewer.current) {
112
+ let calc_zoom = event.deltaY < 0 ? (zoom || 1) * (1 / ZOOM_UNIT) : (zoom || 1) * ZOOM_UNIT;
113
+ if (initZoom * MAX_ZOOM < zoom * ZOOM_UNIT)
114
+ calc_zoom = calc_zoom * ZOOM_UNIT;
115
+ if (initZoom * MIN_ZOOM > zoom * ZOOM_UNIT)
116
+ calc_zoom = calc_zoom * (1 / ZOOM_UNIT);
117
+ const canvas_el = viewer.current;
118
+ const zoomPoint = calculatorZoomPoint(canvas_el, moveX, moveY, dx, dy);
119
+ setZoomX(zoomPoint.x);
120
+ setZoomY(zoomPoint.y);
121
+ setZoom(calc_zoom);
122
+ }
123
+ };
124
+ const handleMouseMove = (event) => {
125
+ if (!hasZoom)
126
+ return;
127
+ if (status === 'MOVE') {
128
+ if (viewer.current) {
129
+ const canvas_el = viewer.current;
130
+ const rect = canvas_el.getBoundingClientRect();
131
+ const mouseX = event.clientX - rect.left;
132
+ const mouseY = event.clientY - rect.top;
133
+ const mouse = getMousePointTransform({ x: mouseX, y: mouseY }, { x: moveX, y: moveY }, { x: zoomX, y: zoomY }, { x: dx, y: dy }, zoom || 1, canvas_el);
134
+ let x = mouse.x;
135
+ let y = mouse.y;
136
+ if (startMousePoint) {
137
+ x = x - startMousePoint.x;
138
+ y = y - startMousePoint.y;
139
+ }
140
+ setMoveX(moveX + x);
141
+ setMoveY(moveY + y);
142
+ const zoomPoint = calculatorZoomPoint(canvas_el, moveX + x, moveY + y, dx, dy);
143
+ setZoomX(zoomPoint.x);
144
+ setZoomY(zoomPoint.y);
145
+ }
146
+ }
147
+ event.preventDefault();
148
+ };
149
+ const handleMouseDown = (event) => {
150
+ setStatus('MOVE');
151
+ if (viewer.current) {
152
+ const canvas_el = viewer.current;
153
+ const rect = canvas_el.getBoundingClientRect();
154
+ const mouseX = event.clientX - rect.left;
155
+ const mouseY = event.clientY - rect.top;
156
+ const mouse = getMousePointTransform({ x: mouseX, y: mouseY }, { x: moveX, y: moveY }, { x: zoomX, y: zoomY }, { x: dx, y: dy }, zoom || 1, canvas_el);
157
+ const x = mouse.x;
158
+ const y = mouse.y;
159
+ setStartMousePoint({
160
+ x: x,
161
+ y: y,
162
+ });
163
+ }
164
+ event.preventDefault();
165
+ };
166
+ const handleMouseUp = (event) => {
167
+ setStatus('STOP');
168
+ event.preventDefault();
169
+ };
170
+ const handleMouseLeave = (event) => {
171
+ setStatus('STOP');
172
+ event.preventDefault();
173
+ };
174
+ return (_jsx("div", { style: {
175
+ width: '100%',
176
+ height: '100%',
177
+ display: 'flex',
178
+ flex: 1,
179
+ backgroundColor: '#f5f5f5'
180
+ }, children: _jsxs("div", { ref: wrapper, onWheel: handleWheel, onMouseMove: handleMouseMove, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onMouseLeave: handleMouseLeave, style: {
181
+ flex: 1,
182
+ position: 'relative'
183
+ }, children: [_jsx("canvas", { ref: viewer, style: {
184
+ position: 'absolute',
185
+ width: '100%',
186
+ height: '100%',
187
+ left: 0,
188
+ top: 0,
189
+ transition: 'all 500ms'
190
+ } }), _jsx(Canvas, { moveX: moveX, moveY: moveY, zoomX: zoomX, zoomY: zoomY, zoom: zoom, dx: dx, dy: dy, dw: dw, dh: dh, coordinates: coordinates }), hasZoom && initZoom !== 0 && (_jsx(hasZoom.ZoomButton, { onClick: initCanvas, children: `${Math.round((zoom / initZoom) * 100)}%` }))] }) }));
191
+ };
192
+ export default XrayViewer;
@@ -0,0 +1 @@
1
+ export const DRAW_LINE_SIZE = 4;
@@ -0,0 +1,124 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ const useResizeObserver = (opts) => {
3
+ const onResize = opts.onResize;
4
+ const onResizeRef = useRef(undefined);
5
+ onResizeRef.current = onResize;
6
+ const round = opts.round || Math.round;
7
+ const resizeObserverRef = useRef({});
8
+ const [size, setSize] = useState({
9
+ width: undefined,
10
+ height: undefined,
11
+ });
12
+ const previous = useRef({
13
+ width: undefined,
14
+ height: undefined,
15
+ });
16
+ const didUnmount = useRef(false);
17
+ useEffect(() => {
18
+ didUnmount.current = false;
19
+ return () => {
20
+ didUnmount.current = true;
21
+ };
22
+ }, []);
23
+ const refCallback = useResolvedElement(useCallback(element => {
24
+ if (!resizeObserverRef.current ||
25
+ resizeObserverRef.current.box !== opts.box ||
26
+ resizeObserverRef.current.round !== round) {
27
+ resizeObserverRef.current = {
28
+ box: opts.box,
29
+ round,
30
+ instance: new ResizeObserver(entries => {
31
+ const entry = entries[0];
32
+ const boxProp = opts.box === 'border-box'
33
+ ? 'borderBoxSize'
34
+ : opts.box === 'device-pixel-content-box'
35
+ ? 'devicePixelContentBoxSize'
36
+ : 'contentBoxSize';
37
+ const reportedWidth = extractSize(entry, boxProp, 'inlineSize');
38
+ const reportedHeight = extractSize(entry, boxProp, 'blockSize');
39
+ const newWidth = reportedWidth ? round(reportedWidth) : undefined;
40
+ const newHeight = reportedHeight ? round(reportedHeight) : undefined;
41
+ if (previous.current.width !== newWidth || previous.current.height !== newHeight) {
42
+ const newSize = { width: newWidth, height: newHeight };
43
+ previous.current.width = newWidth;
44
+ previous.current.height = newHeight;
45
+ if (onResizeRef.current) {
46
+ onResizeRef.current(newSize);
47
+ }
48
+ else {
49
+ if (!didUnmount.current) {
50
+ setSize(newSize);
51
+ }
52
+ }
53
+ }
54
+ }),
55
+ };
56
+ }
57
+ resizeObserverRef.current.instance?.observe(element, { box: opts.box });
58
+ return () => {
59
+ if (resizeObserverRef.current) {
60
+ resizeObserverRef.current.instance?.unobserve(element);
61
+ }
62
+ };
63
+ }, [opts.box, round]), opts.ref);
64
+ return useMemo(() => ({
65
+ ref: refCallback,
66
+ width: size.width,
67
+ height: size.height,
68
+ }), [refCallback, size.width, size.height]);
69
+ };
70
+ export default useResizeObserver;
71
+ function useResolvedElement(subscriber, refOrElement) {
72
+ const lastReportRef = useRef(null);
73
+ const refOrElementRef = useRef(null);
74
+ refOrElementRef.current = refOrElement;
75
+ const cbElementRef = useRef(null);
76
+ useEffect(() => {
77
+ evaluateSubscription();
78
+ });
79
+ const evaluateSubscription = useCallback(() => {
80
+ const cbElement = cbElementRef.current;
81
+ const refOrElement = refOrElementRef.current;
82
+ const element = cbElement
83
+ ? cbElement
84
+ : refOrElement
85
+ ? refOrElement instanceof Element
86
+ ? refOrElement
87
+ : refOrElement.current
88
+ : null;
89
+ if (lastReportRef.current &&
90
+ lastReportRef.current.element === element &&
91
+ lastReportRef.current.subscriber === subscriber) {
92
+ return;
93
+ }
94
+ if (lastReportRef.current && lastReportRef.current.cleanup) {
95
+ lastReportRef.current.cleanup();
96
+ }
97
+ lastReportRef.current = {
98
+ element,
99
+ subscriber,
100
+ cleanup: element ? subscriber(element) : undefined,
101
+ };
102
+ }, [subscriber]);
103
+ useEffect(() => {
104
+ return () => {
105
+ if (lastReportRef.current && lastReportRef.current.cleanup) {
106
+ lastReportRef.current.cleanup();
107
+ lastReportRef.current = null;
108
+ }
109
+ };
110
+ }, []);
111
+ return useCallback(element => {
112
+ cbElementRef.current = element;
113
+ evaluateSubscription();
114
+ }, [evaluateSubscription]);
115
+ }
116
+ function extractSize(entry, boxProp, sizeType) {
117
+ const box = Array.isArray(entry[boxProp])
118
+ ? entry[boxProp][0]
119
+ : undefined;
120
+ if (!box) {
121
+ return entry.contentRect[sizeType === 'inlineSize' ? 'width' : 'height'];
122
+ }
123
+ return box[sizeType];
124
+ }
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { default as XrayViewer } from './components/XrayViewer';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ import { DRAW_LINE_SIZE } from '../constants/graphic';
2
+ // [캔버스] 중앙포인트
3
+ export const canvasCenterPoint = (canvas, image) => {
4
+ const centerX = canvas.width / 2 - image.width / 2;
5
+ const centerY = canvas.height / 2 - image.height / 2;
6
+ return { x: centerX, y: centerY };
7
+ };
8
+ // [캔버스] 그리기
9
+ export const drawCanvas = (moveX, moveY, zoomX, zoomY, zoom, dx, dy, canvas_el, image, isClear) => {
10
+ if (canvas_el) {
11
+ const context = canvas_el.getContext('2d');
12
+ if (isClear)
13
+ context?.clearRect(0, 0, canvas_el.width, canvas_el.height);
14
+ const resolution_ratio_x = canvas_el.width / canvas_el.clientWidth;
15
+ const resolution_ratio_y = canvas_el.height / canvas_el.clientHeight;
16
+ context?.translate(dx * resolution_ratio_x, dy * resolution_ratio_y);
17
+ context?.translate(moveX * resolution_ratio_x, moveY * resolution_ratio_y);
18
+ context?.translate(zoomX, zoomY);
19
+ context?.scale(zoom, zoom);
20
+ context?.translate(-zoomX, -zoomY);
21
+ try {
22
+ if (image)
23
+ context?.drawImage(image, 0, 0, image.width, image.height);
24
+ }
25
+ catch (error) {
26
+ console.error('Failed to draw image', error);
27
+ }
28
+ }
29
+ };
30
+ export const drawRect = (context, coordinate) => {
31
+ setRectangleStyle(context, coordinate);
32
+ const { x, y, width, height } = coordinate;
33
+ context.lineWidth = DRAW_LINE_SIZE;
34
+ context.strokeRect(x, y, width, height);
35
+ };
36
+ export const setRectangleStyle = (context, coordinate) => {
37
+ if (coordinate.color === 'danger') {
38
+ context.strokeStyle = 'rgba(255, 70, 132, 1)';
39
+ }
40
+ if (coordinate.color === 'warning') {
41
+ context.strokeStyle = 'rgba(237, 164, 16, 1)';
42
+ }
43
+ if (coordinate.color === 'success') {
44
+ context.strokeStyle = 'rgba(36, 162, 91, 1)';
45
+ }
46
+ };
47
+ // [캔버스] 해상도
48
+ export const resolutionCanvas = (canvas, width, height) => {
49
+ if (canvas) {
50
+ const canvas_el = canvas;
51
+ canvas_el.width = width ? width : canvas_el.clientWidth;
52
+ canvas_el.height = height ? height : canvas_el.clientHeight;
53
+ return canvas_el;
54
+ }
55
+ else {
56
+ return undefined;
57
+ }
58
+ };
59
+ // [좌표 변형] 원점
60
+ export const __origin = (mouse, origin) => {
61
+ const x = mouse.x - origin.x;
62
+ const y = mouse.y - origin.y;
63
+ return {
64
+ x,
65
+ y,
66
+ };
67
+ };
68
+ // [좌표 변형] 이동
69
+ export const __move = (mouse, move) => {
70
+ const x = mouse.x - move.x;
71
+ const y = mouse.y - move.y;
72
+ return {
73
+ x,
74
+ y,
75
+ };
76
+ };
77
+ // [좌표 변형] 확대, 축소
78
+ export const __zoom = (mouse, zoomPoint, zoom, canvas_el) => {
79
+ const resolution_ratio_x = canvas_el.width / canvas_el.clientWidth;
80
+ const resolution_ratio_y = canvas_el.height / canvas_el.clientHeight;
81
+ let x = mouse.x * (1 / zoom) * resolution_ratio_x;
82
+ let y = mouse.y * (1 / zoom) * resolution_ratio_y;
83
+ x = x + zoomPoint.x;
84
+ y = y + zoomPoint.y;
85
+ x = x - zoomPoint.x * (1 / zoom);
86
+ y = y - zoomPoint.y * (1 / zoom);
87
+ return {
88
+ x,
89
+ y,
90
+ };
91
+ };
92
+ // [좌표 변형] 회전
93
+ export const __rotate = (mouse, rotate, center) => {
94
+ const rad = (rotate * Math.PI) / 180; // Convert degree to radian
95
+ const cos = Math.cos(rad);
96
+ const sin = Math.sin(rad);
97
+ const x = cos * (mouse.x - center.x) - sin * (mouse.y - center.y) + center.x;
98
+ const y = sin * (mouse.x - center.x) + cos * (mouse.y - center.y) + center.y;
99
+ return { x, y };
100
+ };
101
+ /**
102
+ * [좌표 변형] 마우스 포인트 형태 변형
103
+ * 실행 순서 중요(graphic draw 순서와 동일)
104
+ *
105
+ * @param mousePoint: Point 현재 마우스 좌표
106
+ * @param movePoint: Point 이동 좌표
107
+ * @param zoomPoint: Point 확대 좌표
108
+ * @param originPoint: Point 원점 좌표
109
+ * @param zoom: number 확대 비율
110
+ * @param canvas_el: 캔버스 엘리먼트
111
+ * @param rotate: number 회전 각도 0~360
112
+ * @returns 캔버스 크기 해상도에 맞춰 변형된 마우스 좌표
113
+ */
114
+ export const getMousePointTransform = (mousePoint, movePoint, zoomPoint, originPoint, zoom, canvas_el, rotate = 0) => {
115
+ let mouse = mousePoint;
116
+ mouse = __rotate(mouse, rotate, { x: canvas_el.width / 2, y: canvas_el.height / 2 });
117
+ mouse = __origin(mouse, originPoint);
118
+ mouse = __move(mouse, movePoint);
119
+ mouse = __zoom(mouse, zoomPoint, zoom, canvas_el);
120
+ return mouse;
121
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepnoid/canvas",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",