@deepnoid/canvas 0.1.38 → 0.1.39
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.
- package/dist/components/{AnnotationViewer → AnnotationCanvas}/_hooks/useImagePanZoom.d.ts +7 -13
- package/dist/components/{AnnotationViewer → AnnotationCanvas}/_hooks/useImagePanZoom.js +33 -23
- package/dist/components/AnnotationCanvas/_utils/createHistory.d.ts +11 -0
- package/dist/components/AnnotationCanvas/_utils/createHistory.js +34 -0
- package/dist/components/AnnotationCanvas/_utils/panZoom.d.ts +10 -0
- package/dist/components/AnnotationCanvas/_utils/panZoom.js +29 -0
- package/dist/components/AnnotationCanvas/index.d.ts +25 -0
- package/dist/components/AnnotationCanvas/index.js +83 -0
- package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangle.d.ts +5 -0
- package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangle.js +75 -0
- package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangleUtils.d.ts +30 -0
- package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangleUtils.js +211 -0
- package/dist/components/AnnotationLayer/_hooks/drawEvents/useDrawEvents.d.ts +25 -0
- package/dist/components/AnnotationLayer/_hooks/drawEvents/useDrawEvents.js +42 -0
- package/dist/components/AnnotationLayer/_hooks/useCanvasDraw.d.ts +13 -0
- package/dist/components/AnnotationLayer/_hooks/useCanvasDraw.js +115 -0
- package/dist/components/AnnotationLayer/_hooks/useHotkeys.d.ts +7 -0
- package/dist/components/AnnotationLayer/_hooks/useHotkeys.js +15 -0
- package/dist/components/AnnotationLayer/_hooks/useMouse.d.ts +21 -0
- package/dist/components/AnnotationLayer/_hooks/useMouse.js +34 -0
- package/dist/components/AnnotationLayer/index.d.ts +10 -13
- package/dist/components/AnnotationLayer/index.js +116 -22
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.js +8 -0
- package/dist/enum/common.d.ts +13 -0
- package/dist/enum/common.js +15 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -2
- package/dist/types/index.d.ts +58 -0
- package/dist/utils/canvas.d.ts +3 -0
- package/dist/utils/canvas.js +37 -0
- package/dist/utils/common/cloneDeep.d.ts +1 -0
- package/dist/utils/common/cloneDeep.js +18 -0
- package/dist/utils/common/cloneDeepWith.d.ts +1 -0
- package/dist/utils/common/cloneDeepWith.js +34 -0
- package/dist/utils/common/isEqualWith.d.ts +2 -0
- package/dist/utils/common/isEqualWith.js +70 -0
- package/dist/utils/mouseActions.d.ts +8 -0
- package/dist/utils/mouseActions.js +19 -0
- package/dist/utils/pointTransform.d.ts +2 -0
- package/dist/utils/pointTransform.js +46 -0
- package/package.json +2 -1
- package/dist/components/AnnotationViewer/index.d.ts +0 -22
- package/dist/components/AnnotationViewer/index.js +0 -80
- package/dist/constants/graphic.d.ts +0 -1
- package/dist/constants/graphic.js +0 -1
- package/dist/types/coordinate.d.ts +0 -10
- package/dist/utils/graphic.d.ts +0 -48
- package/dist/utils/graphic.js +0 -158
- /package/dist/types/{coordinate.js → index.js} +0 -0
|
@@ -1,35 +1,129 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { DrawMode } from '../../enum/common';
|
|
4
|
+
import { ignoreImageKey, isEqualWith } from '../../utils/common/isEqualWith';
|
|
5
|
+
import { cloneDeepWith } from '../../utils/common/cloneDeepWith';
|
|
6
|
+
import { cloneDeep } from '../../utils/common/cloneDeep';
|
|
7
|
+
import { MouseAction } from '../../utils/mouseActions';
|
|
8
|
+
import { useDrawEvents } from './_hooks/drawEvents/useDrawEvents';
|
|
9
|
+
import { useHotkeys } from './_hooks/useHotkeys';
|
|
10
|
+
import { INIT_POINT, useMouse } from './_hooks/useMouse';
|
|
11
|
+
import { useCanvasDraw } from './_hooks/useCanvasDraw';
|
|
12
|
+
import { drawCanvas, resolutionCanvas } from '../../utils/canvas';
|
|
13
|
+
const initCanvasState = { moveX: 0, moveY: 0, zoomX: 0, zoomY: 0, zoom: 0, dx: 0, dy: 0, dw: 0, dh: 0 };
|
|
14
|
+
const AnnotationLayer = ({ canvasState, coordinates = [], setCoordinates: propSetCoordinates, historyRef, drawing, editable, }) => {
|
|
15
|
+
const { lineSize: drawLineSize, applyStyle, label: drawLabel, mode: selectedDrawMode, color: drawColor } = drawing;
|
|
16
|
+
const canvasRef = useRef(null);
|
|
17
|
+
const canvasStateRef = useRef(initCanvasState);
|
|
18
|
+
const animationFrameId = useRef(0);
|
|
19
|
+
const coordinatesRef = useRef([]);
|
|
20
|
+
const currentCoordinateRef = useRef(null);
|
|
21
|
+
const maskImageRef = useRef(typeof window !== 'undefined' ? new window.Image() : null);
|
|
22
|
+
const mousePointRef = useRef(INIT_POINT);
|
|
23
|
+
const mouseActionRef = useRef(null);
|
|
24
|
+
const startMousePointRef = useRef(null);
|
|
25
|
+
const prevMousePointRef = useRef(null);
|
|
26
|
+
const rectangleAnchorRef = useRef(null);
|
|
27
|
+
const [showSelectedOnly, setShowSelectedOnly] = useState(false);
|
|
28
|
+
const setCoordinates = useCallback((coordinates) => {
|
|
29
|
+
coordinatesRef.current = cloneDeep(coordinates) || [];
|
|
30
|
+
propSetCoordinates(coordinates);
|
|
31
|
+
}, [coordinates]);
|
|
32
|
+
const setMousePoint = useCallback((point) => (mousePointRef.current = point), []);
|
|
33
|
+
const { drawCross, drawActiveRect, drawRectFromPoints, setCursorStyle } = useCanvasDraw({ drawLineSize, drawColor });
|
|
34
|
+
useDrawEvents({
|
|
35
|
+
canvasRef,
|
|
36
|
+
canvasStateRef,
|
|
37
|
+
mousePointRef,
|
|
38
|
+
setMousePoint,
|
|
39
|
+
mouseActionRef,
|
|
40
|
+
startMousePointRef,
|
|
41
|
+
prevMousePointRef,
|
|
42
|
+
currentCoordinateRef,
|
|
43
|
+
coordinatesRef,
|
|
44
|
+
setCoordinates,
|
|
45
|
+
rectangleAnchorRef,
|
|
46
|
+
historyRef,
|
|
47
|
+
maskImageRef,
|
|
48
|
+
drawLabel,
|
|
49
|
+
selectedDrawMode,
|
|
50
|
+
editable,
|
|
51
|
+
});
|
|
52
|
+
const { handleMouseDown, handleMouseUp, handleMouseLeave } = useMouse({
|
|
53
|
+
canvasRef,
|
|
54
|
+
mouseActionRef,
|
|
55
|
+
setMousePoint,
|
|
56
|
+
coordinatesRef,
|
|
57
|
+
setCoordinates,
|
|
58
|
+
historyRef,
|
|
59
|
+
selectedDrawMode,
|
|
60
|
+
});
|
|
61
|
+
useHotkeys({
|
|
62
|
+
onDelete: () => {
|
|
63
|
+
coordinatesRef.current = coordinatesRef.current.filter((coordinate) => !coordinate.selected);
|
|
64
|
+
setCoordinates(coordinatesRef.current);
|
|
65
|
+
historyRef.current.add(cloneDeep(coordinatesRef.current));
|
|
66
|
+
},
|
|
67
|
+
toggleSelectionOnly: () => setShowSelectedOnly((prev) => !prev),
|
|
68
|
+
onUndoRedo: (isRedo) => {
|
|
69
|
+
const result = isRedo ? historyRef.current.redo() : historyRef.current.undo();
|
|
70
|
+
if (result)
|
|
71
|
+
setCoordinates(result);
|
|
72
|
+
const coordinate = coordinatesRef.current.find((coordinate) => coordinate.selected);
|
|
73
|
+
currentCoordinateRef.current = coordinate || null;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
6
76
|
useEffect(() => {
|
|
77
|
+
if (!isEqualWith(coordinates, coordinatesRef.current, ignoreImageKey)) {
|
|
78
|
+
coordinatesRef.current = cloneDeepWith(coordinates, () => {
|
|
79
|
+
return undefined;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}, [coordinates]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const initialize = () => {
|
|
85
|
+
const { moveX, moveY, zoomX, zoomY, zoom, dx, dy, dw, dh } = canvasState;
|
|
86
|
+
canvasStateRef.current = { moveX, moveY, zoomX, zoomY, zoom, dx, dy, dw, dh };
|
|
87
|
+
};
|
|
88
|
+
const drawNewBBox = (context) => {
|
|
89
|
+
if (mouseActionRef.current === MouseAction.LEFT && startMousePointRef.current) {
|
|
90
|
+
drawRectFromPoints(context, mousePointRef.current, startMousePointRef.current, canvasStateRef.current.zoom);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
7
93
|
const redraw = () => {
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
94
|
+
const canvas = resolutionCanvas(canvasRef.current);
|
|
95
|
+
if (!canvas)
|
|
96
|
+
return;
|
|
97
|
+
const { moveX, moveY, zoomX, zoomY, zoom, dx, dy, dw, dh } = canvasState;
|
|
98
|
+
drawCanvas(moveX, moveY, zoomX, zoomY, zoom, dx, dy, canvas, undefined, true);
|
|
99
|
+
const context = canvas.getContext('2d');
|
|
100
|
+
if (context && coordinates) {
|
|
101
|
+
const cloneCoordinates = cloneDeep(coordinatesRef.current);
|
|
102
|
+
cloneCoordinates.reverse().forEach((coordinate) => {
|
|
103
|
+
if (coordinate.type === DrawMode.RECTANGLE) {
|
|
104
|
+
if (!showSelectedOnly || coordinate.selected) {
|
|
105
|
+
applyStyle({ variant: 'drawRect', context, coordinate, drawLineSize, zoom });
|
|
106
|
+
applyStyle({ variant: 'drawText', context, coordinate, drawLineSize, zoom });
|
|
107
|
+
drawActiveRect(context, coordinate, zoom);
|
|
108
|
+
setCursorStyle(canvas, mousePointRef, coordinate, zoom);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
if (mousePointRef.current && editable && selectedDrawMode && !currentCoordinateRef.current) {
|
|
113
|
+
drawNewBBox(context);
|
|
114
|
+
drawCross(context, dw, dh, zoom, mousePointRef.current);
|
|
17
115
|
}
|
|
18
116
|
}
|
|
117
|
+
animationFrameId.current = requestAnimationFrame(redraw);
|
|
19
118
|
};
|
|
119
|
+
initialize();
|
|
20
120
|
redraw();
|
|
21
121
|
window.addEventListener('resize', redraw);
|
|
22
122
|
return () => {
|
|
23
123
|
window.removeEventListener('resize', redraw);
|
|
124
|
+
cancelAnimationFrame(animationFrameId.current);
|
|
24
125
|
};
|
|
25
|
-
}, [
|
|
26
|
-
return (_jsx("canvas", { ref:
|
|
27
|
-
position: 'absolute',
|
|
28
|
-
height: '100%',
|
|
29
|
-
width: '100%',
|
|
30
|
-
backgroundColor: 'transparent',
|
|
31
|
-
transition: 'all 500ms',
|
|
32
|
-
willChange: 'transform',
|
|
33
|
-
} }));
|
|
126
|
+
}, [canvasState, coordinates, selectedDrawMode, editable]);
|
|
127
|
+
return (_jsx("canvas", { ref: canvasRef, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onMouseLeave: handleMouseLeave, style: { position: 'absolute', height: '100%', width: '100%' } }));
|
|
34
128
|
};
|
|
35
129
|
export default AnnotationLayer;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AnnotationCanvasProps } from './AnnotationCanvas/index';
|
|
2
|
+
export type AnnotationViewerProps = Omit<AnnotationCanvasProps, 'editable' | 'drawing' | 'events'> & {
|
|
3
|
+
drawing: Pick<AnnotationCanvasProps['drawing'], 'lineSize' | 'applyStyle'>;
|
|
4
|
+
events: Pick<AnnotationCanvasProps['events'], 'onImageLoadSuccess' | 'onImageLoadError'>;
|
|
5
|
+
};
|
|
6
|
+
export declare const AnnotationViewer: (props: AnnotationViewerProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export type AnnotationEditorProps = AnnotationCanvasProps;
|
|
8
|
+
export declare const AnnotationEditor: (props: AnnotationEditorProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import AnnotationCanvas from './AnnotationCanvas/index';
|
|
3
|
+
export const AnnotationViewer = (props) => {
|
|
4
|
+
return (_jsx(AnnotationCanvas, { ...props, editable: false }));
|
|
5
|
+
};
|
|
6
|
+
export const AnnotationEditor = (props) => {
|
|
7
|
+
return (_jsx(AnnotationCanvas, { ...props, editable: true }));
|
|
8
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare enum DrawMode {
|
|
2
|
+
RECTANGLE = "RECTANGLE"
|
|
3
|
+
}
|
|
4
|
+
export declare enum RectangleAnchor {
|
|
5
|
+
LEFT_TOP = "LEFT_TOP",
|
|
6
|
+
RIGHT_TOP = "RIGHT_TOP",
|
|
7
|
+
LEFT_BOTTOM = "LEFT_BOTTOM",
|
|
8
|
+
RIGHT_BOTTOM = "RIGHT_BOTTOM",
|
|
9
|
+
TOP = "TOP",
|
|
10
|
+
BOTTOM = "BOTTOM",
|
|
11
|
+
LEFT = "LEFT",
|
|
12
|
+
RIGHT = "RIGHT"
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export var DrawMode;
|
|
2
|
+
(function (DrawMode) {
|
|
3
|
+
DrawMode["RECTANGLE"] = "RECTANGLE";
|
|
4
|
+
})(DrawMode || (DrawMode = {}));
|
|
5
|
+
export var RectangleAnchor;
|
|
6
|
+
(function (RectangleAnchor) {
|
|
7
|
+
RectangleAnchor["LEFT_TOP"] = "LEFT_TOP";
|
|
8
|
+
RectangleAnchor["RIGHT_TOP"] = "RIGHT_TOP";
|
|
9
|
+
RectangleAnchor["LEFT_BOTTOM"] = "LEFT_BOTTOM";
|
|
10
|
+
RectangleAnchor["RIGHT_BOTTOM"] = "RIGHT_BOTTOM";
|
|
11
|
+
RectangleAnchor["TOP"] = "TOP";
|
|
12
|
+
RectangleAnchor["BOTTOM"] = "BOTTOM";
|
|
13
|
+
RectangleAnchor["LEFT"] = "LEFT";
|
|
14
|
+
RectangleAnchor["RIGHT"] = "RIGHT";
|
|
15
|
+
})(RectangleAnchor || (RectangleAnchor = {}));
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
3
|
-
export type { ZoomButtonType } from './components/
|
|
4
|
-
export type { Rectangle, Coordinate } from './types
|
|
1
|
+
export { AnnotationViewer, AnnotationEditor } from './components';
|
|
2
|
+
export type { AnnotationViewerProps, AnnotationEditorProps } from './components';
|
|
3
|
+
export type { ZoomButtonType } from './components/AnnotationCanvas';
|
|
4
|
+
export type { Rectangle, Coordinate, ApplyAnnotationStyle } from './types';
|
|
5
|
+
export { DrawMode } from './enum/common';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
1
|
+
export { AnnotationViewer, AnnotationEditor } from './components';
|
|
2
|
+
export { DrawMode } from './enum/common';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { DrawMode } from '../enum/common';
|
|
2
|
+
import { MouseAction } from '../utils/mouseActions';
|
|
3
|
+
export type CanvasState = {
|
|
4
|
+
moveX: number;
|
|
5
|
+
moveY: number;
|
|
6
|
+
zoomX: number;
|
|
7
|
+
zoomY: number;
|
|
8
|
+
zoom: number;
|
|
9
|
+
dx: number;
|
|
10
|
+
dy: number;
|
|
11
|
+
dw: number;
|
|
12
|
+
dh: number;
|
|
13
|
+
};
|
|
14
|
+
export type Point = {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
selected?: boolean;
|
|
18
|
+
mouseAction?: MouseAction;
|
|
19
|
+
};
|
|
20
|
+
export type Rectangle = {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
26
|
+
export type Label = {
|
|
27
|
+
id: number;
|
|
28
|
+
name: string;
|
|
29
|
+
};
|
|
30
|
+
export type Coordinate = {
|
|
31
|
+
label?: Label;
|
|
32
|
+
type?: DrawMode;
|
|
33
|
+
color?: 'success' | 'warning' | 'danger';
|
|
34
|
+
} & Rectangle;
|
|
35
|
+
export type EditableCoordinate = Coordinate & {
|
|
36
|
+
selected?: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type ApplyAnnotationStyleVariant = 'drawRect' | 'drawText';
|
|
39
|
+
export type ApplyAnnotationStyleParams = {
|
|
40
|
+
variant: ApplyAnnotationStyleVariant;
|
|
41
|
+
context: CanvasRenderingContext2D;
|
|
42
|
+
coordinate: EditableCoordinate;
|
|
43
|
+
drawLineSize: number;
|
|
44
|
+
zoom: number;
|
|
45
|
+
};
|
|
46
|
+
export type ApplyAnnotationStyle = (params: ApplyAnnotationStyleParams) => void;
|
|
47
|
+
export type AnnotationCanvasOptionsZoom = {
|
|
48
|
+
step?: number;
|
|
49
|
+
max?: number;
|
|
50
|
+
min?: number;
|
|
51
|
+
};
|
|
52
|
+
export type AnnotationCanvasDrawing = {
|
|
53
|
+
lineSize: number;
|
|
54
|
+
applyStyle: ApplyAnnotationStyle;
|
|
55
|
+
label?: Label;
|
|
56
|
+
mode?: DrawMode;
|
|
57
|
+
color?: string;
|
|
58
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const drawCanvas: (moveX: number, moveY: number, zoomX: number, zoomY: number, zoom: number, dx: number, dy: number, canvas?: HTMLCanvasElement, image?: HTMLImageElement, isClear?: boolean) => void;
|
|
2
|
+
export declare const resolutionCanvas: (canvas: HTMLCanvasElement | null | undefined, width?: number, height?: number) => HTMLCanvasElement | undefined;
|
|
3
|
+
export declare const clearCanvasRect: (canvas: HTMLCanvasElement | null | undefined) => void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const drawCanvas = (moveX, moveY, zoomX, zoomY, zoom, dx, dy, canvas, image, isClear) => {
|
|
2
|
+
if (canvas) {
|
|
3
|
+
const context = canvas.getContext('2d');
|
|
4
|
+
if (isClear)
|
|
5
|
+
context?.clearRect(0, 0, canvas.width, canvas.height);
|
|
6
|
+
const resolution_ratio_x = canvas.width / canvas.clientWidth;
|
|
7
|
+
const resolution_ratio_y = canvas.height / canvas.clientHeight;
|
|
8
|
+
context?.translate(dx * resolution_ratio_x, dy * resolution_ratio_y);
|
|
9
|
+
context?.translate(moveX * resolution_ratio_x, moveY * resolution_ratio_y);
|
|
10
|
+
context?.translate(zoomX, zoomY);
|
|
11
|
+
context?.scale(zoom, zoom);
|
|
12
|
+
context?.translate(-zoomX, -zoomY);
|
|
13
|
+
try {
|
|
14
|
+
if (image)
|
|
15
|
+
context?.drawImage(image, 0, 0, image.width, image.height);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.error('Failed to draw image', error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export const resolutionCanvas = (canvas, width, height) => {
|
|
23
|
+
if (!canvas)
|
|
24
|
+
return undefined;
|
|
25
|
+
const newCanvas = canvas;
|
|
26
|
+
newCanvas.width = width || newCanvas.clientWidth;
|
|
27
|
+
newCanvas.height = height || newCanvas.clientHeight;
|
|
28
|
+
return newCanvas;
|
|
29
|
+
};
|
|
30
|
+
export const clearCanvasRect = (canvas) => {
|
|
31
|
+
if (!canvas)
|
|
32
|
+
return;
|
|
33
|
+
const ctx = canvas.getContext('2d');
|
|
34
|
+
const resolvedCanvas = resolutionCanvas(canvas);
|
|
35
|
+
if (resolvedCanvas)
|
|
36
|
+
ctx?.clearRect(0, 0, resolvedCanvas.width, resolvedCanvas.height);
|
|
37
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const cloneDeep: <T>(obj: T) => T;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const cloneDeep = (obj) => {
|
|
2
|
+
if (obj === null || typeof obj !== 'object') {
|
|
3
|
+
return obj;
|
|
4
|
+
}
|
|
5
|
+
if (obj instanceof Date) {
|
|
6
|
+
return new Date(obj.getTime());
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(obj)) {
|
|
9
|
+
return obj.map((item) => cloneDeep(item));
|
|
10
|
+
}
|
|
11
|
+
const copy = {};
|
|
12
|
+
for (const key in obj) {
|
|
13
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
14
|
+
copy[key] = cloneDeep(obj[key]);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return copy;
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cloneDeepWith<T>(obj: T, cloneValue: (value: unknown, key: PropertyKey | undefined, parent: unknown, stack: Map<unknown, unknown>) => unknown): T;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function cloneDeepWith(obj, cloneValue) {
|
|
2
|
+
const stack = new Map();
|
|
3
|
+
const baseClone = (value, key, parent) => {
|
|
4
|
+
const customResult = cloneValue(value, key, parent, stack);
|
|
5
|
+
if (customResult !== undefined) {
|
|
6
|
+
return customResult;
|
|
7
|
+
}
|
|
8
|
+
if (value === null || typeof value !== 'object') {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
if (stack.has(value)) {
|
|
12
|
+
return stack.get(value);
|
|
13
|
+
}
|
|
14
|
+
if (value instanceof Date) {
|
|
15
|
+
return new Date(value.getTime());
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
const arr = [];
|
|
19
|
+
stack.set(value, arr);
|
|
20
|
+
value.forEach((item, i) => {
|
|
21
|
+
arr[i] = baseClone(item, i, value);
|
|
22
|
+
});
|
|
23
|
+
return arr;
|
|
24
|
+
}
|
|
25
|
+
const result = {};
|
|
26
|
+
stack.set(value, result);
|
|
27
|
+
Object.keys(value).forEach((k) => {
|
|
28
|
+
const v = value[k];
|
|
29
|
+
result[k] = baseClone(v, k, value);
|
|
30
|
+
});
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
return baseClone(obj, undefined, undefined);
|
|
34
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const isEqualWith: (a: unknown, b: unknown, areValuesEqual: (x: unknown, y: unknown, property?: PropertyKey, xParent?: unknown, yParent?: unknown, stack?: Map<unknown, unknown>) => boolean | void) => boolean;
|
|
2
|
+
export declare const ignoreImageKey: (objValue: any, othValue: any) => boolean;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const isEqualDate = (a, b) => a.getTime() === b.getTime();
|
|
2
|
+
const isEqualArray = (a, b, baseEqual, xParent, yParent) => {
|
|
3
|
+
if (a.length !== b.length)
|
|
4
|
+
return false;
|
|
5
|
+
return a.every((item, i) => baseEqual(item, b[i], i, xParent, yParent));
|
|
6
|
+
};
|
|
7
|
+
const isEqualObject = (a, b, baseEqual) => {
|
|
8
|
+
const aKeys = Object.keys(a);
|
|
9
|
+
const bKeys = Object.keys(b);
|
|
10
|
+
if (aKeys.length !== bKeys.length)
|
|
11
|
+
return false;
|
|
12
|
+
return aKeys.every((key) => baseEqual(a[key], b[key], key, a, b));
|
|
13
|
+
};
|
|
14
|
+
export const isEqualWith = (a, b, areValuesEqual) => {
|
|
15
|
+
const stack = new Map();
|
|
16
|
+
const baseEqual = (x, y, property, xParent, yParent) => {
|
|
17
|
+
const customResult = areValuesEqual(x, y, property, xParent, yParent, stack);
|
|
18
|
+
if (typeof customResult === 'boolean')
|
|
19
|
+
return customResult;
|
|
20
|
+
if (x === y)
|
|
21
|
+
return true;
|
|
22
|
+
if (x == null || y == null)
|
|
23
|
+
return false;
|
|
24
|
+
if (typeof x !== typeof y)
|
|
25
|
+
return false;
|
|
26
|
+
if (stack.has(x))
|
|
27
|
+
return stack.get(x) === y;
|
|
28
|
+
stack.set(x, y);
|
|
29
|
+
if (x instanceof Date && y instanceof Date)
|
|
30
|
+
return isEqualDate(x, y);
|
|
31
|
+
if (Array.isArray(x) && Array.isArray(y)) {
|
|
32
|
+
return isEqualArray(x, y, baseEqual, x, y);
|
|
33
|
+
}
|
|
34
|
+
if (typeof x === 'object' && typeof y === 'object' && x !== null && y !== null) {
|
|
35
|
+
return isEqualObject(x, y, baseEqual);
|
|
36
|
+
}
|
|
37
|
+
return Object.is(x, y);
|
|
38
|
+
};
|
|
39
|
+
return baseEqual(a, b);
|
|
40
|
+
};
|
|
41
|
+
export const ignoreImageKey = (objValue, othValue) => {
|
|
42
|
+
if (Array.isArray(objValue) && Array.isArray(othValue)) {
|
|
43
|
+
if (objValue.length !== othValue.length) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
for (let i = 0; i < objValue.length; i++) {
|
|
47
|
+
if (!isEqualWith(objValue[i], othValue[i], ignoreImageKey)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (typeof objValue === 'object' && typeof othValue === 'object' && objValue !== null && othValue !== null) {
|
|
54
|
+
const objKeys = Object.keys(objValue);
|
|
55
|
+
const othKeys = Object.keys(othValue);
|
|
56
|
+
if (objKeys.length !== othKeys.length) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
for (let key of objKeys) {
|
|
60
|
+
if (key === 'image') {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!isEqualWith(objValue[key], othValue[key], ignoreImageKey)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return objValue === othValue;
|
|
70
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare enum MouseAction {
|
|
2
|
+
LEFT = "LEFT",
|
|
3
|
+
WHEEL = "WHEEL",
|
|
4
|
+
RIGHT = "RIGHT"
|
|
5
|
+
}
|
|
6
|
+
export declare const mapButtonToMouseAction: (button: number) => MouseAction | undefined;
|
|
7
|
+
export declare const isMouseClickAction: (button: number, action: MouseAction) => boolean;
|
|
8
|
+
export declare const isMouseDragAction: (buttons: number, action: MouseAction) => boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export var MouseAction;
|
|
2
|
+
(function (MouseAction) {
|
|
3
|
+
MouseAction["LEFT"] = "LEFT";
|
|
4
|
+
MouseAction["WHEEL"] = "WHEEL";
|
|
5
|
+
MouseAction["RIGHT"] = "RIGHT";
|
|
6
|
+
})(MouseAction || (MouseAction = {}));
|
|
7
|
+
const buttonToActionMap = {
|
|
8
|
+
0: MouseAction.LEFT,
|
|
9
|
+
1: MouseAction.WHEEL,
|
|
10
|
+
2: MouseAction.RIGHT,
|
|
11
|
+
};
|
|
12
|
+
const buttonsToActionMap = {
|
|
13
|
+
1: MouseAction.LEFT,
|
|
14
|
+
2: MouseAction.RIGHT,
|
|
15
|
+
4: MouseAction.WHEEL,
|
|
16
|
+
};
|
|
17
|
+
export const mapButtonToMouseAction = (button) => buttonToActionMap[button];
|
|
18
|
+
export const isMouseClickAction = (button, action) => buttonToActionMap[button] === action;
|
|
19
|
+
export const isMouseDragAction = (buttons, action) => (buttons & Number(Object.keys(buttonsToActionMap).find((key) => buttonsToActionMap[Number(key)] === action))) !== 0;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const applyOrigin = (mouse, origin) => {
|
|
2
|
+
const x = mouse.x - origin.x;
|
|
3
|
+
const y = mouse.y - origin.y;
|
|
4
|
+
return {
|
|
5
|
+
x,
|
|
6
|
+
y,
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
const applyMove = (mouse, move) => {
|
|
10
|
+
const x = mouse.x - move.x;
|
|
11
|
+
const y = mouse.y - move.y;
|
|
12
|
+
return {
|
|
13
|
+
x,
|
|
14
|
+
y,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
const applyZoom = (mouse, zoomPoint, zoom, canvas) => {
|
|
18
|
+
const resolution_ratio_x = canvas.width / canvas.clientWidth;
|
|
19
|
+
const resolution_ratio_y = canvas.height / canvas.clientHeight;
|
|
20
|
+
let x = mouse.x * (1 / zoom) * resolution_ratio_x;
|
|
21
|
+
let y = mouse.y * (1 / zoom) * resolution_ratio_y;
|
|
22
|
+
x = x + zoomPoint.x;
|
|
23
|
+
y = y + zoomPoint.y;
|
|
24
|
+
x = x - zoomPoint.x * (1 / zoom);
|
|
25
|
+
y = y - zoomPoint.y * (1 / zoom);
|
|
26
|
+
return {
|
|
27
|
+
x,
|
|
28
|
+
y,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const applyRotate = (mouse, rotate, center) => {
|
|
32
|
+
const rad = (rotate * Math.PI) / 180;
|
|
33
|
+
const cos = Math.cos(rad);
|
|
34
|
+
const sin = Math.sin(rad);
|
|
35
|
+
const x = cos * (mouse.x - center.x) - sin * (mouse.y - center.y) + center.x;
|
|
36
|
+
const y = sin * (mouse.x - center.x) + cos * (mouse.y - center.y) + center.y;
|
|
37
|
+
return { x, y };
|
|
38
|
+
};
|
|
39
|
+
export const getMousePointTransform = (mousePoint, movePoint, zoomPoint, originPoint, zoom, canvas, rotate = 0) => {
|
|
40
|
+
let mouse = mousePoint;
|
|
41
|
+
mouse = applyRotate(mouse, rotate, { x: canvas.width / 2, y: canvas.height / 2 });
|
|
42
|
+
mouse = applyOrigin(mouse, originPoint);
|
|
43
|
+
mouse = applyMove(mouse, movePoint);
|
|
44
|
+
mouse = applyZoom(mouse, zoomPoint, zoom, canvas);
|
|
45
|
+
return mouse;
|
|
46
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepnoid/canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"react-dom": "^19.0.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
+
"@types/node": "^24.5.2",
|
|
29
30
|
"@types/react": "^19.1.12",
|
|
30
31
|
"@types/react-dom": "^19.1.9",
|
|
31
32
|
"@vitejs/plugin-react": "^4.2.0",
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { ComponentType, ReactNode } from 'react';
|
|
2
|
-
import { Coordinate } from '../../types/coordinate';
|
|
3
|
-
export type ZoomButtonType = {
|
|
4
|
-
onClick: () => void;
|
|
5
|
-
children: ReactNode;
|
|
6
|
-
};
|
|
7
|
-
type Props = {
|
|
8
|
-
image: string;
|
|
9
|
-
coordinates?: Coordinate[];
|
|
10
|
-
panZoomable?: boolean;
|
|
11
|
-
ZoomButton?: ComponentType<{
|
|
12
|
-
onClick: () => void;
|
|
13
|
-
children: ReactNode;
|
|
14
|
-
}>;
|
|
15
|
-
resetOnImageChange?: boolean;
|
|
16
|
-
editable?: boolean;
|
|
17
|
-
timeout?: number;
|
|
18
|
-
onImageLoadSuccess?: () => void;
|
|
19
|
-
onImageLoadError?: (error: Error) => void;
|
|
20
|
-
};
|
|
21
|
-
declare const AnnotationViewer: ({ image, coordinates, panZoomable, ZoomButton, resetOnImageChange, editable, timeout, onImageLoadSuccess, onImageLoadError, }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
22
|
-
export default AnnotationViewer;
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import AnnotationLayer from '../AnnotationLayer';
|
|
5
|
-
import { useImagePanZoom } from './_hooks/useImagePanZoom';
|
|
6
|
-
import useResizeObserver from '../../hooks/useResizeObserver';
|
|
7
|
-
import { clearCanvasRect, drawCanvas, resolutionCanvas } from '../../utils/graphic';
|
|
8
|
-
import { useDebounce } from '../../hooks/useDebounce';
|
|
9
|
-
const AnnotationViewer = ({ image, coordinates, panZoomable = false, ZoomButton, resetOnImageChange = true, editable = false, timeout = 10000, onImageLoadSuccess, onImageLoadError, }) => {
|
|
10
|
-
const canvasRef = useRef(null);
|
|
11
|
-
const imageRef = useRef(new Image());
|
|
12
|
-
const [displayCoordinates, setDisplayCoordinates] = useState();
|
|
13
|
-
const debouncedHandleResize = useDebounce((size) => {
|
|
14
|
-
if (image && size.width && size.height && canvasRef.current && imageRef.current.src) {
|
|
15
|
-
const canvas = resolutionCanvas(canvasRef.current);
|
|
16
|
-
if (canvas)
|
|
17
|
-
initZoomAndPosition(canvas, imageRef.current);
|
|
18
|
-
}
|
|
19
|
-
}, 150);
|
|
20
|
-
useResizeObserver({ ref: canvasRef, onResize: debouncedHandleResize });
|
|
21
|
-
const { canvasState, initZoomAndPosition, initCanvas, preserveZoomAndPosition, handleWheel, handleMouseDown, handleMouseMove, handleMouseUp, handleMouseLeave, } = useImagePanZoom({ canvasRef, imageRef, panZoomable });
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
const createEmptyImage = () => {
|
|
24
|
-
const img = new Image();
|
|
25
|
-
img.width = img.height = 0;
|
|
26
|
-
return img;
|
|
27
|
-
};
|
|
28
|
-
const resetCanvas = (errorMsg) => {
|
|
29
|
-
clearCanvasRect(canvasRef.current);
|
|
30
|
-
setDisplayCoordinates([]);
|
|
31
|
-
imageRef.current = createEmptyImage();
|
|
32
|
-
if (errorMsg)
|
|
33
|
-
onImageLoadError?.(new Error(errorMsg));
|
|
34
|
-
};
|
|
35
|
-
if (!image?.trim())
|
|
36
|
-
return void resetCanvas();
|
|
37
|
-
let cancelled = false;
|
|
38
|
-
const tempImage = new Image();
|
|
39
|
-
tempImage.onload = () => {
|
|
40
|
-
if (cancelled || !canvasRef.current)
|
|
41
|
-
return;
|
|
42
|
-
onImageLoadSuccess?.();
|
|
43
|
-
if (resetOnImageChange) {
|
|
44
|
-
setTimeout(() => initCanvas(), 0);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
preserveZoomAndPosition(canvasRef.current, tempImage, canvasState.zoom / (canvasState.initZoom || 1));
|
|
48
|
-
}
|
|
49
|
-
imageRef.current = tempImage;
|
|
50
|
-
setDisplayCoordinates(coordinates);
|
|
51
|
-
};
|
|
52
|
-
tempImage.onerror = () => !cancelled && resetCanvas(`Failed to load image: ${image}`);
|
|
53
|
-
tempImage.src = image;
|
|
54
|
-
return () => {
|
|
55
|
-
cancelled = true;
|
|
56
|
-
};
|
|
57
|
-
}, [image, resetOnImageChange]);
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
if (!canvasRef.current)
|
|
60
|
-
return;
|
|
61
|
-
const redraw = (canvas) => {
|
|
62
|
-
drawCanvas(canvasState.moveX, canvasState.moveY, canvasState.zoomX, canvasState.zoomY, canvasState.zoom, canvasState.dx, canvasState.dy, canvas, imageRef.current, true);
|
|
63
|
-
};
|
|
64
|
-
const canvas = resolutionCanvas(canvasRef.current);
|
|
65
|
-
if (canvas)
|
|
66
|
-
redraw(canvas);
|
|
67
|
-
}, [canvasState]);
|
|
68
|
-
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, onContextMenu: (e) => e.preventDefault(), style: {
|
|
69
|
-
flex: 1,
|
|
70
|
-
position: 'relative',
|
|
71
|
-
cursor: panZoomable ? 'grab' : 'default',
|
|
72
|
-
}, children: [_jsx("canvas", { ref: canvasRef, style: {
|
|
73
|
-
position: 'absolute',
|
|
74
|
-
width: '100%',
|
|
75
|
-
height: '100%',
|
|
76
|
-
left: 0,
|
|
77
|
-
top: 0,
|
|
78
|
-
} }), _jsx(AnnotationLayer, { ...canvasState, coordinates: displayCoordinates, editable: editable }), ZoomButton && canvasState.initZoom > 0 && (_jsx(ZoomButton, { onClick: initCanvas, children: `${Math.round((canvasState.zoom / canvasState.initZoom) * 100)}%` }))] }) }));
|
|
79
|
-
};
|
|
80
|
-
export default AnnotationViewer;
|