@deepnoid/canvas 0.1.58 → 0.1.59
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/README.md +177 -3
- package/dist/engine/annotation/annotationTypes.d.ts +21 -0
- package/dist/engine/annotation/annotationTypes.js +6 -0
- package/dist/engine/annotation/rectangle/rectangleController.d.ts +26 -0
- package/dist/engine/annotation/rectangle/rectangleController.js +115 -0
- package/dist/engine/annotation/rectangle/rectangleHitTest.d.ts +7 -0
- package/dist/engine/annotation/rectangle/rectangleHitTest.js +102 -0
- package/dist/engine/annotation/rectangle/rectangleInteraction.d.ts +17 -0
- package/dist/engine/annotation/rectangle/rectangleInteraction.js +30 -0
- package/dist/engine/annotation/rectangle/rectangleMath.d.ts +10 -0
- package/dist/engine/annotation/rectangle/rectangleMath.js +29 -0
- package/dist/engine/annotation/rectangle/rectangleRenderer.d.ts +5 -0
- package/dist/engine/annotation/rectangle/rectangleRenderer.js +91 -0
- package/dist/engine/annotation/rectangle/rectangleTransform.d.ts +14 -0
- package/dist/engine/annotation/rectangle/rectangleTransform.js +65 -0
- package/dist/{enum/common.d.ts → engine/annotation/rectangle/rectangleTypes.d.ts} +0 -3
- package/dist/{enum/common.js → engine/annotation/rectangle/rectangleTypes.js} +0 -4
- package/dist/engine/history.d.ts +11 -0
- package/dist/{components/AnnotationCanvas/_utils/createHistory.js → engine/history.js} +4 -4
- package/dist/engine/interaction/drawModeRouter.d.ts +3 -0
- package/dist/engine/interaction/drawModeRouter.js +56 -0
- package/dist/engine/interaction/interactionController.d.ts +13 -0
- package/dist/engine/interaction/interactionController.js +53 -0
- package/dist/engine/interaction/interface.d.ts +15 -0
- package/dist/engine/interaction/panZoomInteraction.d.ts +3 -0
- package/dist/engine/interaction/panZoomInteraction.js +29 -0
- package/dist/engine/interaction/pointerInteraction.d.ts +16 -0
- package/dist/engine/interaction/pointerInteraction.js +48 -0
- package/dist/engine/pan-zoom/panZoomController.d.ts +26 -0
- package/dist/engine/pan-zoom/panZoomController.js +148 -0
- package/dist/engine/pan-zoom/panZoomUtils.d.ts +10 -0
- package/dist/engine/pan-zoom/panZoomUtils.js +24 -0
- package/dist/engine/public/annotationEngine.d.ts +75 -0
- package/dist/engine/public/annotationEngine.js +257 -0
- package/dist/engine/renderer/drawCross.d.ts +2 -0
- package/dist/engine/renderer/drawCross.js +19 -0
- package/dist/engine/renderer/interface.d.ts +4 -0
- package/dist/engine/renderer/interface.js +1 -0
- package/dist/{types/index.d.ts → engine/types.d.ts} +12 -21
- package/dist/engine/types.js +1 -0
- package/dist/engine/utils/mousePoint.d.ts +3 -0
- package/dist/engine/utils/mousePoint.js +52 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +2 -2
- package/dist/{components/AnnotationCanvas/index.d.ts → react/AnnotationCanvas.d.ts} +7 -6
- package/dist/react/AnnotationCanvas.js +110 -0
- package/dist/{components → react}/index.d.ts +1 -1
- package/dist/{components → react}/index.js +1 -1
- package/package.json +1 -1
- package/dist/components/AnnotationCanvas/_hooks/useImagePanZoom.d.ts +0 -24
- package/dist/components/AnnotationCanvas/_hooks/useImagePanZoom.js +0 -143
- package/dist/components/AnnotationCanvas/_utils/createHistory.d.ts +0 -11
- package/dist/components/AnnotationCanvas/_utils/panZoom.d.ts +0 -10
- package/dist/components/AnnotationCanvas/_utils/panZoom.js +0 -29
- package/dist/components/AnnotationCanvas/index.js +0 -96
- package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangle.d.ts +0 -5
- package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangle.js +0 -88
- package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangleUtils.d.ts +0 -28
- package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangleUtils.js +0 -204
- package/dist/components/AnnotationLayer/_hooks/drawEvents/useDrawEvents.d.ts +0 -25
- package/dist/components/AnnotationLayer/_hooks/drawEvents/useDrawEvents.js +0 -43
- package/dist/components/AnnotationLayer/_hooks/useCanvasDraw.d.ts +0 -13
- package/dist/components/AnnotationLayer/_hooks/useCanvasDraw.js +0 -115
- package/dist/components/AnnotationLayer/index.d.ts +0 -14
- package/dist/components/AnnotationLayer/index.js +0 -122
- package/dist/utils/canvas.d.ts +0 -3
- package/dist/utils/canvas.js +0 -37
- package/dist/utils/pointTransform.d.ts +0 -2
- package/dist/utils/pointTransform.js +0 -46
- /package/dist/{types/index.js → engine/interaction/interface.js} +0 -0
- /package/dist/{utils/common → engine/utils}/cloneDeep.d.ts +0 -0
- /package/dist/{utils/common → engine/utils}/cloneDeep.js +0 -0
- /package/dist/{utils/common → engine/utils}/deepEqual.d.ts +0 -0
- /package/dist/{utils/common → engine/utils}/deepEqual.js +0 -0
- /package/dist/{utils/common → engine/utils}/isEqualWith.d.ts +0 -0
- /package/dist/{utils/common → engine/utils}/isEqualWith.js +0 -0
- /package/dist/{utils → engine/utils}/mouseActions.d.ts +0 -0
- /package/dist/{utils → engine/utils}/mouseActions.js +0 -0
- /package/dist/{hooks → react/hooks}/useDebounce.d.ts +0 -0
- /package/dist/{hooks → react/hooks}/useDebounce.js +0 -0
- /package/dist/{components/AnnotationLayer/_hooks → react/hooks}/useHotkeys.d.ts +0 -0
- /package/dist/{components/AnnotationLayer/_hooks → react/hooks}/useHotkeys.js +0 -0
- /package/dist/{hooks → react/hooks}/useResizeObserver.d.ts +0 -0
- /package/dist/{hooks → react/hooks}/useResizeObserver.js +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { createHistory } from '../history';
|
|
2
|
+
import { PanZoomController } from '../pan-zoom/panZoomController';
|
|
3
|
+
import { cloneDeep } from '../utils/cloneDeep';
|
|
4
|
+
import { InteractionController } from '../interaction/interactionController';
|
|
5
|
+
import { panZoomInteraction } from '../interaction/panZoomInteraction';
|
|
6
|
+
import { rectangleRenderer } from '../annotation/rectangle/rectangleRenderer';
|
|
7
|
+
import { PointerInteraction } from '../interaction/pointerInteraction';
|
|
8
|
+
import { drawModeRouter } from '../interaction/drawModeRouter';
|
|
9
|
+
export class AnnotationEngine {
|
|
10
|
+
constructor(params) {
|
|
11
|
+
this.history = null;
|
|
12
|
+
this.editable = true;
|
|
13
|
+
this.renderers = {
|
|
14
|
+
RECTANGLE: rectangleRenderer,
|
|
15
|
+
// POLYGON: polygonRenderer,
|
|
16
|
+
};
|
|
17
|
+
this.imageCanvas = params.imageCanvas;
|
|
18
|
+
this.image = params.image;
|
|
19
|
+
this.imageCanvasState = params.imageCanvasState;
|
|
20
|
+
this.annotationsCanvas = params.annotationsCanvas;
|
|
21
|
+
this.annotations = params.annotations;
|
|
22
|
+
this.setAnnotationsCallback = params.setAnnotations;
|
|
23
|
+
this.drawing = params.drawing;
|
|
24
|
+
this.enableHotkeys = params.enableHotkeys;
|
|
25
|
+
this.editable = params.editable ?? true;
|
|
26
|
+
this.onChange = params.onChange;
|
|
27
|
+
this.showSelectedOnly = false;
|
|
28
|
+
this.initHistory(this.annotations);
|
|
29
|
+
this.panZoom = new PanZoomController(this.imageCanvas, this.image, () => this.imageCanvasState, (next) => {
|
|
30
|
+
this.imageCanvasState = next;
|
|
31
|
+
this.onChange?.();
|
|
32
|
+
}, {
|
|
33
|
+
enabled: params.panZoomEnabled ?? true,
|
|
34
|
+
zoom: params.zoom,
|
|
35
|
+
editable: this.editable,
|
|
36
|
+
});
|
|
37
|
+
this.interactionController = new InteractionController([
|
|
38
|
+
new PointerInteraction(this),
|
|
39
|
+
panZoomInteraction(this.panZoom),
|
|
40
|
+
drawModeRouter(this),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
// ----------------- State -----------------
|
|
44
|
+
getImageCanvas() {
|
|
45
|
+
return this.imageCanvas;
|
|
46
|
+
}
|
|
47
|
+
getImageCanvasState() {
|
|
48
|
+
return this.imageCanvasState;
|
|
49
|
+
}
|
|
50
|
+
getAnnotationsCanvas() {
|
|
51
|
+
return this.annotationsCanvas;
|
|
52
|
+
}
|
|
53
|
+
getDrawing() {
|
|
54
|
+
return this.drawing;
|
|
55
|
+
}
|
|
56
|
+
setDrawing(drawing) {
|
|
57
|
+
this.drawing = drawing;
|
|
58
|
+
}
|
|
59
|
+
isEditable() {
|
|
60
|
+
return this.editable;
|
|
61
|
+
}
|
|
62
|
+
setEditable(editable) {
|
|
63
|
+
this.editable = editable;
|
|
64
|
+
this.panZoom.setEditable(editable);
|
|
65
|
+
}
|
|
66
|
+
setPanZoomEnabled(enabled) {
|
|
67
|
+
this.panZoom.setEnabled(enabled);
|
|
68
|
+
}
|
|
69
|
+
getEnableHotkeys() {
|
|
70
|
+
return this.enableHotkeys;
|
|
71
|
+
}
|
|
72
|
+
getShowSelectedOnly() {
|
|
73
|
+
return this.showSelectedOnly;
|
|
74
|
+
}
|
|
75
|
+
destroy() {
|
|
76
|
+
this.annotations = [];
|
|
77
|
+
this.history = null;
|
|
78
|
+
this.panZoom.destroy();
|
|
79
|
+
}
|
|
80
|
+
// ----------------- Image Canvas Pan Zoom -----------------
|
|
81
|
+
getZoomRatioLabel() {
|
|
82
|
+
return `${Math.round((this.imageCanvasState.zoom / this.imageCanvasState.initZoom) * 100)}%`;
|
|
83
|
+
}
|
|
84
|
+
initImageCanvas(resetZoom) {
|
|
85
|
+
this.setupCanvasResolution(this.imageCanvas);
|
|
86
|
+
this.setupCanvasResolution(this.annotationsCanvas);
|
|
87
|
+
if (resetZoom) {
|
|
88
|
+
this.panZoom.resetZoomAndPosition();
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.panZoom.preserveZoomAndPosition();
|
|
92
|
+
}
|
|
93
|
+
this.drawCanvasAll();
|
|
94
|
+
}
|
|
95
|
+
setupCanvasResolution(canvas) {
|
|
96
|
+
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
97
|
+
const boundingRect = canvas.getBoundingClientRect();
|
|
98
|
+
canvas.width = boundingRect.width * devicePixelRatio;
|
|
99
|
+
canvas.height = boundingRect.height * devicePixelRatio;
|
|
100
|
+
const context = canvas.getContext('2d');
|
|
101
|
+
if (!context)
|
|
102
|
+
return;
|
|
103
|
+
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
|
|
104
|
+
}
|
|
105
|
+
// ----------------- draw Canvas -----------------
|
|
106
|
+
drawCanvasAll() {
|
|
107
|
+
this.drawImageCanvas();
|
|
108
|
+
this.drawAnnotationsCanvas();
|
|
109
|
+
}
|
|
110
|
+
drawImageCanvas() {
|
|
111
|
+
this.drawCanvas(this.imageCanvas, this.image);
|
|
112
|
+
}
|
|
113
|
+
drawAnnotationsCanvas() {
|
|
114
|
+
this.drawCanvas(this.annotationsCanvas, undefined);
|
|
115
|
+
}
|
|
116
|
+
drawCanvas(canvas, image) {
|
|
117
|
+
const { moveX, moveY, zoomX, zoomY, dx, dy, zoom } = this.getImageCanvasState();
|
|
118
|
+
const context = canvas.getContext('2d');
|
|
119
|
+
if (!context)
|
|
120
|
+
return;
|
|
121
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
122
|
+
context.save();
|
|
123
|
+
context.translate(dx, dy);
|
|
124
|
+
context.translate(moveX, moveY);
|
|
125
|
+
context.translate(zoomX, zoomY);
|
|
126
|
+
context.scale(zoom, zoom);
|
|
127
|
+
context.translate(-zoomX, -zoomY);
|
|
128
|
+
if (image) {
|
|
129
|
+
context.drawImage(image, 0, 0, image.naturalWidth, image.naturalHeight);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.drawAnnotations(context);
|
|
133
|
+
}
|
|
134
|
+
context.restore();
|
|
135
|
+
}
|
|
136
|
+
clearCanvasAll() {
|
|
137
|
+
this.clearCanvas(this.imageCanvas);
|
|
138
|
+
this.clearCanvas(this.annotationsCanvas);
|
|
139
|
+
}
|
|
140
|
+
clearCanvas(canvas) {
|
|
141
|
+
const context = canvas.getContext('2d');
|
|
142
|
+
if (!context)
|
|
143
|
+
return;
|
|
144
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
145
|
+
}
|
|
146
|
+
resetCanvas() {
|
|
147
|
+
this.clearCanvasAll();
|
|
148
|
+
this.setAnnotations([]);
|
|
149
|
+
}
|
|
150
|
+
// ----------------- Annotations -----------------
|
|
151
|
+
getAnnotations() {
|
|
152
|
+
return this.annotations;
|
|
153
|
+
}
|
|
154
|
+
setAnnotations(annotation) {
|
|
155
|
+
this.annotations = cloneDeep(annotation);
|
|
156
|
+
this.syncAnnotations();
|
|
157
|
+
}
|
|
158
|
+
appendAnnotation(annotation) {
|
|
159
|
+
this.annotations.push(annotation);
|
|
160
|
+
this.syncAnnotations();
|
|
161
|
+
}
|
|
162
|
+
syncAnnotations() {
|
|
163
|
+
this.setAnnotationsCallback(this.annotations);
|
|
164
|
+
this.drawAnnotationsCanvas();
|
|
165
|
+
}
|
|
166
|
+
drawAnnotations(context) {
|
|
167
|
+
const mode = this.getDrawing().mode;
|
|
168
|
+
if (!mode)
|
|
169
|
+
return;
|
|
170
|
+
const renderer = this.renderers[mode];
|
|
171
|
+
if (!renderer)
|
|
172
|
+
return;
|
|
173
|
+
renderer.draw(context, this);
|
|
174
|
+
}
|
|
175
|
+
getSelectedAnnotation() {
|
|
176
|
+
return this.annotations.find((a) => a.selected) ?? null;
|
|
177
|
+
}
|
|
178
|
+
setSelectedAnnotation(target) {
|
|
179
|
+
let changed = false;
|
|
180
|
+
for (const annotation of this.annotations) {
|
|
181
|
+
const nextSelected = target !== null && annotation === target;
|
|
182
|
+
if (annotation.selected !== nextSelected) {
|
|
183
|
+
annotation.selected = nextSelected;
|
|
184
|
+
changed = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (changed) {
|
|
188
|
+
this.drawAnnotationsCanvas();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// ----------------- Interaction -----------------
|
|
192
|
+
getPointerRenderState() {
|
|
193
|
+
return (this.interactionController
|
|
194
|
+
.getInteractions()
|
|
195
|
+
.find((i) => i instanceof PointerInteraction)
|
|
196
|
+
?.getRenderState() ?? null);
|
|
197
|
+
}
|
|
198
|
+
getDrawRenderState() {
|
|
199
|
+
const mode = this.drawing.mode;
|
|
200
|
+
if (!mode)
|
|
201
|
+
return null;
|
|
202
|
+
return this.interactionController.getRenderState(mode);
|
|
203
|
+
}
|
|
204
|
+
onMouseDown(e) {
|
|
205
|
+
this.interactionController.handleMouseDown(e);
|
|
206
|
+
}
|
|
207
|
+
onMouseMove(e) {
|
|
208
|
+
const layers = this.interactionController.handleMouseMove(e);
|
|
209
|
+
if (layers.imageCanvas) {
|
|
210
|
+
this.drawImageCanvas();
|
|
211
|
+
}
|
|
212
|
+
if (layers.annotationsCanvas) {
|
|
213
|
+
this.drawAnnotationsCanvas();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
onMouseUp(e) {
|
|
217
|
+
this.interactionController.handleMouseUp(e);
|
|
218
|
+
}
|
|
219
|
+
onMouseLeave(e) {
|
|
220
|
+
this.interactionController.handleMouseLeave(e);
|
|
221
|
+
}
|
|
222
|
+
onWheel(e) {
|
|
223
|
+
this.interactionController.handleWheel(e);
|
|
224
|
+
this.drawCanvasAll();
|
|
225
|
+
}
|
|
226
|
+
// ----------------- History -----------------
|
|
227
|
+
initHistory(annotations) {
|
|
228
|
+
this.history = createHistory();
|
|
229
|
+
if (annotations.length > 0) {
|
|
230
|
+
this.history.init(cloneDeep(annotations));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
commitHistory(checkInit = false) {
|
|
234
|
+
if (!this.history)
|
|
235
|
+
return;
|
|
236
|
+
if (checkInit && this.history.getHistoryIndex() === -1) {
|
|
237
|
+
this.history.addInit();
|
|
238
|
+
}
|
|
239
|
+
this.history.add(cloneDeep(this.annotations));
|
|
240
|
+
}
|
|
241
|
+
deleteSelected() {
|
|
242
|
+
const next = this.annotations.filter((c) => !c.selected);
|
|
243
|
+
if (next.length === this.annotations.length)
|
|
244
|
+
return;
|
|
245
|
+
this.setAnnotations(next);
|
|
246
|
+
this.commitHistory();
|
|
247
|
+
}
|
|
248
|
+
toggleShowSelectedOnly() {
|
|
249
|
+
this.showSelectedOnly = !this.showSelectedOnly;
|
|
250
|
+
this.drawAnnotationsCanvas();
|
|
251
|
+
}
|
|
252
|
+
undoRedo(isRedo) {
|
|
253
|
+
const result = isRedo ? this.history?.redo() : this.history?.undo();
|
|
254
|
+
if (result)
|
|
255
|
+
this.setAnnotations(result);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function drawCross(context, mousePoint, dw, dh, zoom, drawLineSize) {
|
|
2
|
+
if (mousePoint.x > 0 && mousePoint.y > 0 && mousePoint.x < dw && mousePoint.y < dh) {
|
|
3
|
+
if (context)
|
|
4
|
+
context.lineWidth = (drawLineSize / 2) * (1 / zoom);
|
|
5
|
+
context?.setLineDash([5]);
|
|
6
|
+
context?.beginPath();
|
|
7
|
+
context?.moveTo(mousePoint.x, 0);
|
|
8
|
+
context?.lineTo(mousePoint.x, dh);
|
|
9
|
+
if (context)
|
|
10
|
+
context.strokeStyle = '#001f3f';
|
|
11
|
+
context?.stroke();
|
|
12
|
+
context?.beginPath();
|
|
13
|
+
context?.moveTo(0, mousePoint.y);
|
|
14
|
+
context?.lineTo(dw, mousePoint.y);
|
|
15
|
+
if (context)
|
|
16
|
+
context.strokeStyle = '#001f3f';
|
|
17
|
+
context?.stroke();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DrawMode } from '
|
|
2
|
-
import { MouseAction } from '
|
|
3
|
-
export type
|
|
1
|
+
import { Annotation, DrawMode } from './annotation/annotationTypes';
|
|
2
|
+
import { MouseAction } from './utils/mouseActions';
|
|
3
|
+
export type ImageCanvasState = {
|
|
4
4
|
moveX: number;
|
|
5
5
|
moveY: number;
|
|
6
6
|
zoomX: number;
|
|
@@ -10,44 +10,30 @@ export type CanvasState = {
|
|
|
10
10
|
dy: number;
|
|
11
11
|
dw: number;
|
|
12
12
|
dh: number;
|
|
13
|
+
initZoom: number;
|
|
13
14
|
};
|
|
14
15
|
export type Point = {
|
|
15
16
|
x: number;
|
|
16
17
|
y: number;
|
|
18
|
+
};
|
|
19
|
+
export type PointState = Point & {
|
|
17
20
|
selected?: boolean;
|
|
18
21
|
mouseAction?: MouseAction;
|
|
19
22
|
};
|
|
20
|
-
export type Rectangle = {
|
|
21
|
-
x: number;
|
|
22
|
-
y: number;
|
|
23
|
-
width: number;
|
|
24
|
-
height: number;
|
|
25
|
-
};
|
|
26
23
|
export type Label = {
|
|
27
24
|
id: number;
|
|
28
25
|
name: string;
|
|
29
26
|
type: string;
|
|
30
27
|
};
|
|
31
|
-
export type Coordinate = {
|
|
32
|
-
label?: Label;
|
|
33
|
-
type?: DrawMode;
|
|
34
|
-
color?: 'success' | 'warning' | 'danger';
|
|
35
|
-
selected?: boolean;
|
|
36
|
-
} & Rectangle;
|
|
37
28
|
export type ApplyAnnotationStyleVariant = 'drawRect' | 'drawText';
|
|
38
29
|
export type ApplyAnnotationStyleParams = {
|
|
39
30
|
variant: ApplyAnnotationStyleVariant;
|
|
40
31
|
context: CanvasRenderingContext2D;
|
|
41
|
-
|
|
32
|
+
annotation: Annotation;
|
|
42
33
|
drawLineSize: number;
|
|
43
34
|
zoom: number;
|
|
44
35
|
};
|
|
45
36
|
export type ApplyAnnotationStyle = (params: ApplyAnnotationStyleParams) => void;
|
|
46
|
-
export type AnnotationCanvasOptionsZoom = {
|
|
47
|
-
step?: number;
|
|
48
|
-
max?: number;
|
|
49
|
-
min?: number;
|
|
50
|
-
};
|
|
51
37
|
export type AnnotationCanvasDrawing = {
|
|
52
38
|
lineSize: number;
|
|
53
39
|
applyStyle: ApplyAnnotationStyle;
|
|
@@ -55,3 +41,8 @@ export type AnnotationCanvasDrawing = {
|
|
|
55
41
|
mode?: DrawMode;
|
|
56
42
|
color?: string;
|
|
57
43
|
};
|
|
44
|
+
export type AnnotationCanvasOptionsZoom = {
|
|
45
|
+
step?: number;
|
|
46
|
+
max?: number;
|
|
47
|
+
min?: number;
|
|
48
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { ImageCanvasState, Point, PointState } from '../types';
|
|
2
|
+
export declare function getCanvasMousePoint(event: MouseEvent, canvas: HTMLCanvasElement, canvasState: ImageCanvasState): Point;
|
|
3
|
+
export declare function getMousePointTransform(mousePoint: PointState, movePoint: PointState, zoomPoint: PointState, originPoint: PointState, zoom: number, canvas: HTMLCanvasElement, rotate?: number): Point;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function getCanvasMousePoint(event, canvas, canvasState) {
|
|
2
|
+
const rect = canvas.getBoundingClientRect();
|
|
3
|
+
const { moveX, moveY, zoomX, zoomY, zoom, dx, dy } = canvasState;
|
|
4
|
+
const mouseX = event.clientX - rect.left;
|
|
5
|
+
const mouseY = event.clientY - rect.top;
|
|
6
|
+
return getMousePointTransform({ x: mouseX, y: mouseY, selected: false }, { x: moveX, y: moveY, selected: false }, { x: zoomX, y: zoomY, selected: false }, { x: dx, y: dy, selected: false }, zoom, canvas);
|
|
7
|
+
}
|
|
8
|
+
export function getMousePointTransform(mousePoint, movePoint, zoomPoint, originPoint, zoom, canvas, rotate = 0) {
|
|
9
|
+
let mouse = mousePoint;
|
|
10
|
+
mouse = applyRotate(mouse, rotate, {
|
|
11
|
+
x: canvas.clientWidth / 2,
|
|
12
|
+
y: canvas.clientHeight / 2,
|
|
13
|
+
});
|
|
14
|
+
mouse = applyOrigin(mouse, originPoint);
|
|
15
|
+
mouse = applyMove(mouse, movePoint);
|
|
16
|
+
mouse = applyZoom(mouse, zoomPoint, zoom);
|
|
17
|
+
return mouse;
|
|
18
|
+
}
|
|
19
|
+
function applyOrigin(mouse, origin) {
|
|
20
|
+
return {
|
|
21
|
+
x: mouse.x - origin.x,
|
|
22
|
+
y: mouse.y - origin.y,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function applyMove(mouse, move) {
|
|
26
|
+
return {
|
|
27
|
+
x: mouse.x - move.x,
|
|
28
|
+
y: mouse.y - move.y,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function applyZoom(mouse, zoomPoint, zoom) {
|
|
32
|
+
// 1. 줌 기준점을 원점으로 이동
|
|
33
|
+
let x = mouse.x - zoomPoint.x;
|
|
34
|
+
let y = mouse.y - zoomPoint.y;
|
|
35
|
+
// 2. 줌 역변환 (화면 좌표 → 이미지 좌표)
|
|
36
|
+
x = x / zoom;
|
|
37
|
+
y = y / zoom;
|
|
38
|
+
// 3. 줌 기준점만큼 다시 이동
|
|
39
|
+
x = x + zoomPoint.x;
|
|
40
|
+
y = y + zoomPoint.y;
|
|
41
|
+
return { x, y };
|
|
42
|
+
}
|
|
43
|
+
function applyRotate(mouse, rotate, center) {
|
|
44
|
+
if (rotate === 0)
|
|
45
|
+
return mouse;
|
|
46
|
+
const rad = (rotate * Math.PI) / 180;
|
|
47
|
+
const cos = Math.cos(rad);
|
|
48
|
+
const sin = Math.sin(rad);
|
|
49
|
+
const x = cos * (mouse.x - center.x) - sin * (mouse.y - center.y) + center.x;
|
|
50
|
+
const y = sin * (mouse.x - center.x) + cos * (mouse.y - center.y) + center.y;
|
|
51
|
+
return { x, y };
|
|
52
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
export { AnnotationViewer, AnnotationEditor } from './
|
|
2
|
-
export type {
|
|
3
|
-
export type {
|
|
4
|
-
export
|
|
5
|
-
export { DrawMode } from './enum/common';
|
|
1
|
+
export { AnnotationViewer, AnnotationEditor } from './react';
|
|
2
|
+
export type { Label, ApplyAnnotationStyle } from './engine/types';
|
|
3
|
+
export type { Rectangle, Annotation } from './engine/annotation/annotationTypes';
|
|
4
|
+
export { DrawMode } from './engine/annotation/annotationTypes';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { AnnotationViewer, AnnotationEditor } from './
|
|
2
|
-
export { DrawMode } from './
|
|
1
|
+
export { AnnotationViewer, AnnotationEditor } from './react';
|
|
2
|
+
export { DrawMode } from './engine/annotation/annotationTypes';
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { ComponentType, Dispatch, ReactNode, SetStateAction } from 'react';
|
|
2
|
-
import { AnnotationCanvasDrawing, AnnotationCanvasOptionsZoom
|
|
3
|
-
|
|
2
|
+
import { AnnotationCanvasDrawing, AnnotationCanvasOptionsZoom } from '../engine/types';
|
|
3
|
+
import { Annotation } from '../engine/annotation/annotationTypes';
|
|
4
|
+
type ZoomButtonType = {
|
|
4
5
|
onClick: () => void;
|
|
5
6
|
children: ReactNode;
|
|
6
7
|
};
|
|
7
8
|
export type AnnotationCanvasProps = {
|
|
8
9
|
image: string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
options
|
|
10
|
+
annotations?: Annotation[];
|
|
11
|
+
setAnnotations?: Dispatch<SetStateAction<Annotation[] | undefined>>;
|
|
12
|
+
options?: {
|
|
12
13
|
panZoomEnabled?: boolean;
|
|
13
14
|
zoom?: AnnotationCanvasOptionsZoom;
|
|
14
15
|
ZoomButton?: ComponentType<ZoomButtonType>;
|
|
@@ -23,5 +24,5 @@ export type AnnotationCanvasProps = {
|
|
|
23
24
|
enableHotkeys?: boolean;
|
|
24
25
|
editable?: boolean;
|
|
25
26
|
};
|
|
26
|
-
declare const AnnotationCanvas: ({ image,
|
|
27
|
+
declare const AnnotationCanvas: ({ image, annotations, setAnnotations, options, drawing, events, enableHotkeys, editable, }: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
|
|
27
28
|
export default AnnotationCanvas;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { AnnotationEngine } from '../engine/public/annotationEngine';
|
|
5
|
+
import useResizeObserver from './hooks/useResizeObserver';
|
|
6
|
+
import { useDebounce } from './hooks/useDebounce';
|
|
7
|
+
import { useHotkeys } from './hooks/useHotkeys';
|
|
8
|
+
const AnnotationCanvas = ({ image, annotations = [], setAnnotations, options, drawing, events, enableHotkeys = true, editable = true, }) => {
|
|
9
|
+
const { panZoomEnabled, zoom, ZoomButton, resetOnImageChange = false } = options || {};
|
|
10
|
+
const { onImageLoadSuccess, onImageLoadError } = events || {};
|
|
11
|
+
const imageCanvasRef = useRef(null);
|
|
12
|
+
const imageRef = useRef(new Image());
|
|
13
|
+
const engineRef = useRef(null);
|
|
14
|
+
const annotationsCanvasRef = useRef(null);
|
|
15
|
+
const [_, forceRender] = useState(0);
|
|
16
|
+
useResizeObserver({
|
|
17
|
+
ref: imageCanvasRef,
|
|
18
|
+
onResize: useDebounce((size) => {
|
|
19
|
+
const engine = engineRef.current;
|
|
20
|
+
const imageCanvas = engine?.getImageCanvas();
|
|
21
|
+
if (!engine || !imageCanvas || !imageRef.current.src)
|
|
22
|
+
return;
|
|
23
|
+
const needsResize = size.width !== imageCanvas.width || size.height !== imageCanvas.height;
|
|
24
|
+
if (needsResize)
|
|
25
|
+
engine.initImageCanvas(true);
|
|
26
|
+
}, 150),
|
|
27
|
+
});
|
|
28
|
+
useHotkeys({
|
|
29
|
+
onDelete: () => engineRef.current?.deleteSelected(),
|
|
30
|
+
toggleSelectionOnly: () => engineRef.current?.toggleShowSelectedOnly(),
|
|
31
|
+
onUndoRedo: (isRedo) => engineRef.current?.undoRedo(isRedo),
|
|
32
|
+
enabled: enableHotkeys,
|
|
33
|
+
});
|
|
34
|
+
const createEmptyImage = () => {
|
|
35
|
+
const img = new Image();
|
|
36
|
+
img.width = img.height = 0;
|
|
37
|
+
return img;
|
|
38
|
+
};
|
|
39
|
+
const resetCanvas = (errorMsg) => {
|
|
40
|
+
engineRef.current?.resetCanvas();
|
|
41
|
+
imageRef.current = createEmptyImage();
|
|
42
|
+
if (errorMsg)
|
|
43
|
+
onImageLoadError?.(new Error(errorMsg));
|
|
44
|
+
};
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!image?.trim())
|
|
47
|
+
return void resetCanvas();
|
|
48
|
+
let cancelled = false;
|
|
49
|
+
const img = new Image();
|
|
50
|
+
img.onload = () => {
|
|
51
|
+
if (cancelled || !imageCanvasRef.current || !annotationsCanvasRef.current)
|
|
52
|
+
return;
|
|
53
|
+
const imageCanvasState = engineRef.current
|
|
54
|
+
? engineRef.current.getImageCanvasState()
|
|
55
|
+
: { moveX: 0, moveY: 0, zoomX: 0, zoomY: 0, zoom: 1, dx: 0, dy: 0, dw: img.width, dh: img.height, initZoom: 1 };
|
|
56
|
+
engineRef.current?.destroy?.();
|
|
57
|
+
engineRef.current = new AnnotationEngine({
|
|
58
|
+
imageCanvas: imageCanvasRef.current,
|
|
59
|
+
image: img,
|
|
60
|
+
imageCanvasState,
|
|
61
|
+
annotationsCanvas: annotationsCanvasRef.current,
|
|
62
|
+
annotations,
|
|
63
|
+
setAnnotations: (annotations) => setAnnotations?.(annotations),
|
|
64
|
+
drawing,
|
|
65
|
+
editable,
|
|
66
|
+
panZoomEnabled,
|
|
67
|
+
zoom,
|
|
68
|
+
enableHotkeys,
|
|
69
|
+
onChange: () => forceRender((v) => v + 1),
|
|
70
|
+
});
|
|
71
|
+
engineRef.current.initImageCanvas(resetOnImageChange);
|
|
72
|
+
};
|
|
73
|
+
img.onerror = () => !cancelled && resetCanvas(`Failed to load image: ${image}`);
|
|
74
|
+
img.src = image;
|
|
75
|
+
imageRef.current = img;
|
|
76
|
+
onImageLoadSuccess?.();
|
|
77
|
+
return () => {
|
|
78
|
+
cancelled = true;
|
|
79
|
+
img.onload = null;
|
|
80
|
+
img.onerror = null;
|
|
81
|
+
engineRef.current?.destroy();
|
|
82
|
+
};
|
|
83
|
+
}, [image]);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!engineRef.current)
|
|
86
|
+
return;
|
|
87
|
+
engineRef.current.setPanZoomEnabled(!!panZoomEnabled);
|
|
88
|
+
}, [panZoomEnabled]);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!engineRef.current)
|
|
91
|
+
return;
|
|
92
|
+
engineRef.current.setEditable(editable);
|
|
93
|
+
}, [editable]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!engineRef.current)
|
|
96
|
+
return;
|
|
97
|
+
const before = engineRef.current.getAnnotations();
|
|
98
|
+
if (JSON.stringify(before) !== JSON.stringify(annotations)) {
|
|
99
|
+
engineRef.current.setAnnotations(annotations);
|
|
100
|
+
engineRef.current.commitHistory();
|
|
101
|
+
}
|
|
102
|
+
}, [engineRef.current, annotations]);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!engineRef.current)
|
|
105
|
+
return;
|
|
106
|
+
engineRef.current.setDrawing(drawing);
|
|
107
|
+
}, [engineRef.current, drawing]);
|
|
108
|
+
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: { flex: 1, position: 'relative', cursor: !panZoomEnabled || editable ? 'default' : 'grab' }, 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() }))] }) }));
|
|
109
|
+
};
|
|
110
|
+
export default AnnotationCanvas;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnnotationCanvasProps } from './AnnotationCanvas
|
|
1
|
+
import { AnnotationCanvasProps } from './AnnotationCanvas';
|
|
2
2
|
export type AnnotationViewerProps = Omit<AnnotationCanvasProps, 'editable' | 'drawing' | 'events'> & {
|
|
3
3
|
drawing: Pick<AnnotationCanvasProps['drawing'], 'lineSize' | 'applyStyle'>;
|
|
4
4
|
events: Pick<AnnotationCanvasProps['events'], 'onImageLoadSuccess' | 'onImageLoadError'>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import AnnotationCanvas from './AnnotationCanvas
|
|
2
|
+
import AnnotationCanvas from './AnnotationCanvas';
|
|
3
3
|
export const AnnotationViewer = (props) => {
|
|
4
4
|
return (_jsx(AnnotationCanvas, { ...props, editable: false }));
|
|
5
5
|
};
|
package/package.json
CHANGED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { RefObject, WheelEvent, MouseEvent } from 'react';
|
|
2
|
-
import { AnnotationCanvasOptionsZoom, CanvasState } from '../../../types';
|
|
3
|
-
type ImageCanvasState = CanvasState & {
|
|
4
|
-
initZoom: number;
|
|
5
|
-
};
|
|
6
|
-
type Props = {
|
|
7
|
-
canvasRef: RefObject<HTMLCanvasElement | null>;
|
|
8
|
-
imageRef: RefObject<HTMLImageElement>;
|
|
9
|
-
panZoomEnabled?: boolean;
|
|
10
|
-
zoom?: AnnotationCanvasOptionsZoom;
|
|
11
|
-
editable?: boolean;
|
|
12
|
-
};
|
|
13
|
-
export declare function useImagePanZoom({ canvasRef, imageRef, panZoomEnabled, zoom, editable }: Props): {
|
|
14
|
-
canvasState: ImageCanvasState;
|
|
15
|
-
initZoomAndPosition: (canvas: HTMLCanvasElement, image: HTMLImageElement) => void;
|
|
16
|
-
preserveZoomAndPosition: (canvas: HTMLCanvasElement, image: HTMLImageElement, zoomRatio: number) => void;
|
|
17
|
-
initCanvas: () => void;
|
|
18
|
-
handleWheel: (event: WheelEvent) => void;
|
|
19
|
-
handleMouseDown: (event: MouseEvent) => void;
|
|
20
|
-
handleMouseMove: (event: MouseEvent) => void;
|
|
21
|
-
handleMouseUp: (event: MouseEvent) => void;
|
|
22
|
-
handleMouseLeave: (event: MouseEvent) => void;
|
|
23
|
-
};
|
|
24
|
-
export {};
|