@deepnoid/canvas 0.1.57 → 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.
Files changed (84) hide show
  1. package/README.md +177 -3
  2. package/dist/engine/annotation/annotationTypes.d.ts +21 -0
  3. package/dist/engine/annotation/annotationTypes.js +6 -0
  4. package/dist/engine/annotation/rectangle/rectangleController.d.ts +26 -0
  5. package/dist/engine/annotation/rectangle/rectangleController.js +115 -0
  6. package/dist/engine/annotation/rectangle/rectangleHitTest.d.ts +7 -0
  7. package/dist/engine/annotation/rectangle/rectangleHitTest.js +102 -0
  8. package/dist/engine/annotation/rectangle/rectangleInteraction.d.ts +17 -0
  9. package/dist/engine/annotation/rectangle/rectangleInteraction.js +30 -0
  10. package/dist/engine/annotation/rectangle/rectangleMath.d.ts +10 -0
  11. package/dist/engine/annotation/rectangle/rectangleMath.js +29 -0
  12. package/dist/engine/annotation/rectangle/rectangleRenderer.d.ts +5 -0
  13. package/dist/engine/annotation/rectangle/rectangleRenderer.js +91 -0
  14. package/dist/engine/annotation/rectangle/rectangleTransform.d.ts +14 -0
  15. package/dist/engine/annotation/rectangle/rectangleTransform.js +65 -0
  16. package/dist/{enum/common.d.ts → engine/annotation/rectangle/rectangleTypes.d.ts} +0 -3
  17. package/dist/{enum/common.js → engine/annotation/rectangle/rectangleTypes.js} +0 -4
  18. package/dist/engine/history.d.ts +11 -0
  19. package/dist/{components/AnnotationCanvas/_utils/createHistory.js → engine/history.js} +4 -4
  20. package/dist/engine/interaction/drawModeRouter.d.ts +3 -0
  21. package/dist/engine/interaction/drawModeRouter.js +56 -0
  22. package/dist/engine/interaction/interactionController.d.ts +13 -0
  23. package/dist/engine/interaction/interactionController.js +53 -0
  24. package/dist/engine/interaction/interface.d.ts +15 -0
  25. package/dist/engine/interaction/panZoomInteraction.d.ts +3 -0
  26. package/dist/engine/interaction/panZoomInteraction.js +29 -0
  27. package/dist/engine/interaction/pointerInteraction.d.ts +16 -0
  28. package/dist/engine/interaction/pointerInteraction.js +48 -0
  29. package/dist/engine/pan-zoom/panZoomController.d.ts +26 -0
  30. package/dist/engine/pan-zoom/panZoomController.js +148 -0
  31. package/dist/engine/pan-zoom/panZoomUtils.d.ts +10 -0
  32. package/dist/engine/pan-zoom/panZoomUtils.js +24 -0
  33. package/dist/engine/public/annotationEngine.d.ts +75 -0
  34. package/dist/engine/public/annotationEngine.js +257 -0
  35. package/dist/engine/renderer/drawCross.d.ts +2 -0
  36. package/dist/engine/renderer/drawCross.js +19 -0
  37. package/dist/engine/renderer/interface.d.ts +4 -0
  38. package/dist/engine/renderer/interface.js +1 -0
  39. package/dist/{types/index.d.ts → engine/types.d.ts} +12 -21
  40. package/dist/engine/types.js +1 -0
  41. package/dist/engine/utils/mousePoint.d.ts +3 -0
  42. package/dist/engine/utils/mousePoint.js +52 -0
  43. package/dist/index.d.ts +4 -5
  44. package/dist/index.js +2 -2
  45. package/dist/{components/AnnotationCanvas/index.d.ts → react/AnnotationCanvas.d.ts} +7 -6
  46. package/dist/react/AnnotationCanvas.js +110 -0
  47. package/dist/{components → react}/index.d.ts +1 -1
  48. package/dist/{components → react}/index.js +1 -1
  49. package/package.json +1 -1
  50. package/dist/components/AnnotationCanvas/_hooks/useImagePanZoom.d.ts +0 -24
  51. package/dist/components/AnnotationCanvas/_hooks/useImagePanZoom.js +0 -143
  52. package/dist/components/AnnotationCanvas/_utils/createHistory.d.ts +0 -11
  53. package/dist/components/AnnotationCanvas/_utils/panZoom.d.ts +0 -10
  54. package/dist/components/AnnotationCanvas/_utils/panZoom.js +0 -29
  55. package/dist/components/AnnotationCanvas/index.js +0 -96
  56. package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangle.d.ts +0 -5
  57. package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangle.js +0 -88
  58. package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangleUtils.d.ts +0 -28
  59. package/dist/components/AnnotationLayer/_hooks/drawEvents/rectangleUtils.js +0 -204
  60. package/dist/components/AnnotationLayer/_hooks/drawEvents/useDrawEvents.d.ts +0 -25
  61. package/dist/components/AnnotationLayer/_hooks/drawEvents/useDrawEvents.js +0 -43
  62. package/dist/components/AnnotationLayer/_hooks/useCanvasDraw.d.ts +0 -13
  63. package/dist/components/AnnotationLayer/_hooks/useCanvasDraw.js +0 -115
  64. package/dist/components/AnnotationLayer/index.d.ts +0 -14
  65. package/dist/components/AnnotationLayer/index.js +0 -122
  66. package/dist/utils/canvas.d.ts +0 -3
  67. package/dist/utils/canvas.js +0 -37
  68. package/dist/utils/pointTransform.d.ts +0 -2
  69. package/dist/utils/pointTransform.js +0 -46
  70. /package/dist/{types/index.js → engine/interaction/interface.js} +0 -0
  71. /package/dist/{utils/common → engine/utils}/cloneDeep.d.ts +0 -0
  72. /package/dist/{utils/common → engine/utils}/cloneDeep.js +0 -0
  73. /package/dist/{utils/common → engine/utils}/deepEqual.d.ts +0 -0
  74. /package/dist/{utils/common → engine/utils}/deepEqual.js +0 -0
  75. /package/dist/{utils/common → engine/utils}/isEqualWith.d.ts +0 -0
  76. /package/dist/{utils/common → engine/utils}/isEqualWith.js +0 -0
  77. /package/dist/{utils → engine/utils}/mouseActions.d.ts +0 -0
  78. /package/dist/{utils → engine/utils}/mouseActions.js +0 -0
  79. /package/dist/{hooks → react/hooks}/useDebounce.d.ts +0 -0
  80. /package/dist/{hooks → react/hooks}/useDebounce.js +0 -0
  81. /package/dist/{components/AnnotationLayer/_hooks → react/hooks}/useHotkeys.d.ts +0 -0
  82. /package/dist/{components/AnnotationLayer/_hooks → react/hooks}/useHotkeys.js +0 -0
  83. /package/dist/{hooks → react/hooks}/useResizeObserver.d.ts +0 -0
  84. /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,2 @@
1
+ import { Point } from '../types';
2
+ export declare function drawCross(context: CanvasRenderingContext2D, mousePoint: Point, dw: number, dh: number, zoom: number, drawLineSize: number): void;
@@ -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,4 @@
1
+ import { AnnotationEngine } from '../public/annotationEngine';
2
+ export interface AnnotationsRenderer {
3
+ draw(ctx: CanvasRenderingContext2D, engine: AnnotationEngine): void;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,6 @@
1
- import { DrawMode } from '../enum/common';
2
- import { MouseAction } from '../utils/mouseActions';
3
- export type CanvasState = {
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
- coordinate: Coordinate;
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 './components';
2
- export type { AnnotationViewerProps, AnnotationEditorProps } from './components';
3
- export type { ZoomButtonType } from './components/AnnotationCanvas';
4
- export type { Rectangle, Coordinate, Label, ApplyAnnotationStyle } from './types';
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 './components';
2
- export { DrawMode } from './enum/common';
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, Coordinate } from '../../types';
3
- export type ZoomButtonType = {
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
- coordinates?: Coordinate[];
10
- setCoordinates?: Dispatch<SetStateAction<Coordinate[] | undefined>>;
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, coordinates, setCoordinates, options, drawing, events, enableHotkeys, editable, }: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
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/index';
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/index';
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepnoid/canvas",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",
@@ -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 {};