@annotorious/annotorious 3.0.0-rc.1
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 +6 -0
- package/dist/Annotorious.d.ts +15 -0
- package/dist/Annotorious.d.ts.map +1 -0
- package/dist/AnnotoriousOpts.d.ts +14 -0
- package/dist/AnnotoriousOpts.d.ts.map +1 -0
- package/dist/annotation/SVGAnnotationLayer.svelte.d.ts +1 -0
- package/dist/annotation/SVGAnnotationLayerPointerEvent.d.ts +11 -0
- package/dist/annotation/SVGAnnotationLayerPointerEvent.d.ts.map +1 -0
- package/dist/annotation/Transform.d.ts +6 -0
- package/dist/annotation/Transform.d.ts.map +1 -0
- package/dist/annotation/editors/Editor.svelte.d.ts +1 -0
- package/dist/annotation/editors/EditorMount.svelte.d.ts +1 -0
- package/dist/annotation/editors/Handle.d.ts +14 -0
- package/dist/annotation/editors/Handle.d.ts.map +1 -0
- package/dist/annotation/editors/editorsRegistry.d.ts +5 -0
- package/dist/annotation/editors/editorsRegistry.d.ts.map +1 -0
- package/dist/annotation/editors/index.d.ts +7 -0
- package/dist/annotation/editors/index.d.ts.map +1 -0
- package/dist/annotation/editors/polygon/PolygonEditor.svelte.d.ts +1 -0
- package/dist/annotation/editors/polygon/index.d.ts +2 -0
- package/dist/annotation/editors/polygon/index.d.ts.map +1 -0
- package/dist/annotation/editors/rectangle/RectangleEditor.svelte.d.ts +1 -0
- package/dist/annotation/editors/rectangle/index.d.ts +2 -0
- package/dist/annotation/editors/rectangle/index.d.ts.map +1 -0
- package/dist/annotation/index.d.ts +7 -0
- package/dist/annotation/index.d.ts.map +1 -0
- package/dist/annotation/shapes/Ellipse.svelte.d.ts +1 -0
- package/dist/annotation/shapes/Polygon.svelte.d.ts +1 -0
- package/dist/annotation/shapes/Rectangle.svelte.d.ts +1 -0
- package/dist/annotation/shapes/index.d.ts +4 -0
- package/dist/annotation/shapes/index.d.ts.map +1 -0
- package/dist/annotation/tools/DrawingToolConfig.d.ts +8 -0
- package/dist/annotation/tools/DrawingToolConfig.d.ts.map +1 -0
- package/dist/annotation/tools/ToolMount.svelte.d.ts +1 -0
- package/dist/annotation/tools/drawingToolsRegistry.d.ts +17 -0
- package/dist/annotation/tools/drawingToolsRegistry.d.ts.map +1 -0
- package/dist/annotation/tools/index.d.ts +5 -0
- package/dist/annotation/tools/index.d.ts.map +1 -0
- package/dist/annotation/tools/polygon/RubberbandPolygon.svelte.d.ts +1 -0
- package/dist/annotation/tools/polygon/index.d.ts +2 -0
- package/dist/annotation/tools/polygon/index.d.ts.map +1 -0
- package/dist/annotation/tools/rectangle/RubberbandRectangle.svelte.d.ts +1 -0
- package/dist/annotation/tools/rectangle/index.d.ts +2 -0
- package/dist/annotation/tools/rectangle/index.d.ts.map +1 -0
- package/dist/annotation/utils/index.d.ts +4 -0
- package/dist/annotation/utils/index.d.ts.map +1 -0
- package/dist/annotation/utils/math.d.ts +2 -0
- package/dist/annotation/utils/math.d.ts.map +1 -0
- package/dist/annotation/utils/responsive.d.ts +5 -0
- package/dist/annotation/utils/responsive.d.ts.map +1 -0
- package/dist/annotation/utils/styling.d.ts +4 -0
- package/dist/annotation/utils/styling.d.ts.map +1 -0
- package/dist/annotation/utils/touch.d.ts +2 -0
- package/dist/annotation/utils/touch.d.ts.map +1 -0
- package/dist/annotorious.css +1 -0
- package/dist/annotorious.es.js +3890 -0
- package/dist/annotorious.es.js.map +1 -0
- package/dist/annotorious.js +2 -0
- package/dist/annotorious.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/model/core/ImageAnnotation.d.ts +9 -0
- package/dist/model/core/ImageAnnotation.d.ts.map +1 -0
- package/dist/model/core/Shape.d.ts +20 -0
- package/dist/model/core/Shape.d.ts.map +1 -0
- package/dist/model/core/ellipse/Ellipse.d.ts +12 -0
- package/dist/model/core/ellipse/Ellipse.d.ts.map +1 -0
- package/dist/model/core/ellipse/ellipseUtils.d.ts +2 -0
- package/dist/model/core/ellipse/ellipseUtils.d.ts.map +1 -0
- package/dist/model/core/ellipse/index.d.ts +3 -0
- package/dist/model/core/ellipse/index.d.ts.map +1 -0
- package/dist/model/core/index.d.ts +7 -0
- package/dist/model/core/index.d.ts.map +1 -0
- package/dist/model/core/polygon/Polygon.d.ts +9 -0
- package/dist/model/core/polygon/Polygon.d.ts.map +1 -0
- package/dist/model/core/polygon/index.d.ts +3 -0
- package/dist/model/core/polygon/index.d.ts.map +1 -0
- package/dist/model/core/polygon/polygonUtils.d.ts +2 -0
- package/dist/model/core/polygon/polygonUtils.d.ts.map +1 -0
- package/dist/model/core/rectangle/Rectangle.d.ts +12 -0
- package/dist/model/core/rectangle/Rectangle.d.ts.map +1 -0
- package/dist/model/core/rectangle/index.d.ts +3 -0
- package/dist/model/core/rectangle/index.d.ts.map +1 -0
- package/dist/model/core/rectangle/rectangleUtils.d.ts +4 -0
- package/dist/model/core/rectangle/rectangleUtils.d.ts.map +1 -0
- package/dist/model/core/shapeUtils.d.ts +35 -0
- package/dist/model/core/shapeUtils.d.ts.map +1 -0
- package/dist/model/index.d.ts +3 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/w3c/W3CImageFormatAdapter.d.ts +7 -0
- package/dist/model/w3c/W3CImageFormatAdapter.d.ts.map +1 -0
- package/dist/model/w3c/fragment/FragmentSelector.d.ts +10 -0
- package/dist/model/w3c/fragment/FragmentSelector.d.ts.map +1 -0
- package/dist/model/w3c/fragment/index.d.ts +2 -0
- package/dist/model/w3c/fragment/index.d.ts.map +1 -0
- package/dist/model/w3c/index.d.ts +4 -0
- package/dist/model/w3c/index.d.ts.map +1 -0
- package/dist/model/w3c/svg/SVG.d.ts +5 -0
- package/dist/model/w3c/svg/SVG.d.ts.map +1 -0
- package/dist/model/w3c/svg/SVGSelector.d.ts +9 -0
- package/dist/model/w3c/svg/SVGSelector.d.ts.map +1 -0
- package/dist/model/w3c/svg/index.d.ts +2 -0
- package/dist/model/w3c/svg/index.d.ts.map +1 -0
- package/dist/state/ImageAnnotationStore.d.ts +11 -0
- package/dist/state/ImageAnnotationStore.d.ts.map +1 -0
- package/dist/state/ImageAnnotatorState.d.ts +12 -0
- package/dist/state/ImageAnnotatorState.d.ts.map +1 -0
- package/dist/state/index.d.ts +3 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/spatialTree.d.ts +21 -0
- package/dist/state/spatialTree.d.ts.map +1 -0
- package/dist/themes/index.d.ts +2 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/smart/index.d.ts +2 -0
- package/dist/themes/smart/index.d.ts.map +1 -0
- package/dist/themes/smart/setTheme.d.ts +3 -0
- package/dist/themes/smart/setTheme.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/Annotorious.css +74 -0
- package/src/Annotorious.ts +158 -0
- package/src/AnnotoriousOpts.ts +40 -0
- package/src/annotation/SVGAnnotationLayer.svelte +169 -0
- package/src/annotation/SVGAnnotationLayerPointerEvent.ts +55 -0
- package/src/annotation/Transform.ts +24 -0
- package/src/annotation/editors/Editor.svelte +61 -0
- package/src/annotation/editors/EditorMount.svelte +44 -0
- package/src/annotation/editors/Handle.ts +21 -0
- package/src/annotation/editors/editorsRegistry.ts +14 -0
- package/src/annotation/editors/index.ts +7 -0
- package/src/annotation/editors/polygon/PolygonEditor.svelte +64 -0
- package/src/annotation/editors/polygon/index.ts +1 -0
- package/src/annotation/editors/rectangle/RectangleEditor.svelte +143 -0
- package/src/annotation/editors/rectangle/index.ts +1 -0
- package/src/annotation/index.ts +7 -0
- package/src/annotation/shapes/Ellipse.svelte +32 -0
- package/src/annotation/shapes/Polygon.svelte +26 -0
- package/src/annotation/shapes/Rectangle.svelte +32 -0
- package/src/annotation/shapes/index.ts +3 -0
- package/src/annotation/tools/DrawingToolConfig.ts +9 -0
- package/src/annotation/tools/ToolMount.svelte +49 -0
- package/src/annotation/tools/drawingToolsRegistry.ts +26 -0
- package/src/annotation/tools/index.ts +4 -0
- package/src/annotation/tools/polygon/RubberbandPolygon.svelte +165 -0
- package/src/annotation/tools/polygon/index.ts +1 -0
- package/src/annotation/tools/rectangle/RubberbandRectangle.svelte +131 -0
- package/src/annotation/tools/rectangle/index.ts +1 -0
- package/src/annotation/utils/index.ts +3 -0
- package/src/annotation/utils/math.ts +6 -0
- package/src/annotation/utils/responsive.ts +56 -0
- package/src/annotation/utils/styling.ts +19 -0
- package/src/annotation/utils/touch.ts +1 -0
- package/src/index.ts +20 -0
- package/src/model/core/ImageAnnotation.ts +14 -0
- package/src/model/core/Shape.ts +37 -0
- package/src/model/core/ellipse/Ellipse.ts +21 -0
- package/src/model/core/ellipse/ellipseUtils.ts +28 -0
- package/src/model/core/ellipse/index.ts +2 -0
- package/src/model/core/index.ts +6 -0
- package/src/model/core/polygon/Polygon.ts +15 -0
- package/src/model/core/polygon/index.ts +2 -0
- package/src/model/core/polygon/polygonUtils.ts +43 -0
- package/src/model/core/rectangle/Rectangle.ts +21 -0
- package/src/model/core/rectangle/index.ts +2 -0
- package/src/model/core/rectangle/rectangleUtils.ts +17 -0
- package/src/model/core/shapeUtils.ts +57 -0
- package/src/model/index.ts +2 -0
- package/src/model/w3c/W3CImageFormatAdapter.ts +83 -0
- package/src/model/w3c/fragment/FragmentSelector.ts +59 -0
- package/src/model/w3c/fragment/index.ts +1 -0
- package/src/model/w3c/index.ts +3 -0
- package/src/model/w3c/svg/SVG.ts +36 -0
- package/src/model/w3c/svg/SVGSelector.ts +99 -0
- package/src/model/w3c/svg/index.ts +1 -0
- package/src/state/ImageAnnotationStore.ts +18 -0
- package/src/state/ImageAnnotatorState.ts +88 -0
- package/src/state/index.ts +2 -0
- package/src/state/spatialTree.ts +108 -0
- package/src/themes/dark/index.css +24 -0
- package/src/themes/index.ts +1 -0
- package/src/themes/light/index.css +30 -0
- package/src/themes/smart/index.ts +1 -0
- package/src/themes/smart/setTheme.ts +46 -0
- package/src/vite-env.d.ts +2 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { SvelteComponent } from 'svelte';
|
|
2
|
+
import type { Annotator, DrawingStyle, User } from '@annotorious/core';
|
|
3
|
+
import { createAnonymousGuest, createBaseAnnotator, createLifecyleObserver } from '@annotorious/core';
|
|
4
|
+
import { registerEditor } from './annotation/editors';
|
|
5
|
+
import { getTool, registerTool, listDrawingTools, type DrawingTool } from './annotation/tools';
|
|
6
|
+
import { SVGAnnotationLayer } from './annotation';
|
|
7
|
+
import type { DrawingToolOpts, SVGAnnotationLayerPointerEvent } from './annotation';
|
|
8
|
+
import type { ImageAnnotation, ShapeType } from './model';
|
|
9
|
+
import { createSvelteImageAnnotatorState } from './state';
|
|
10
|
+
import { setTheme } from './themes';
|
|
11
|
+
import { fillDefaults } from './AnnotoriousOpts';
|
|
12
|
+
import type { AnnotoriousOpts } from './AnnotoriousOpts';
|
|
13
|
+
|
|
14
|
+
import './Annotorious.css';
|
|
15
|
+
import './themes/dark/index.css';
|
|
16
|
+
import './themes/light/index.css';
|
|
17
|
+
|
|
18
|
+
export interface ImageAnnotator<E extends unknown = ImageAnnotation> extends Annotator<ImageAnnotation, E> {
|
|
19
|
+
|
|
20
|
+
listDrawingTools(): string[];
|
|
21
|
+
|
|
22
|
+
registerDrawingTool(name: string, tool: typeof SvelteComponent, opts?: DrawingToolOpts): void;
|
|
23
|
+
|
|
24
|
+
registerShapeEditor(shapeType: ShapeType, editor: typeof SvelteComponent): void;
|
|
25
|
+
|
|
26
|
+
setDrawingTool(tool: DrawingTool): void;
|
|
27
|
+
|
|
28
|
+
setDrawingEnabled(enabled: boolean): void;
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const createImageAnnotator = <E extends unknown = ImageAnnotation>(
|
|
33
|
+
image: string | HTMLImageElement | HTMLCanvasElement,
|
|
34
|
+
options: AnnotoriousOpts<ImageAnnotation, E> = {}
|
|
35
|
+
): ImageAnnotator<E> => {
|
|
36
|
+
|
|
37
|
+
if (!image)
|
|
38
|
+
throw 'Missing argument: image';
|
|
39
|
+
|
|
40
|
+
const img = (typeof image === 'string' ?
|
|
41
|
+
document.getElementById(image) : image) as HTMLImageElement | HTMLCanvasElement;
|
|
42
|
+
|
|
43
|
+
const opts = fillDefaults<ImageAnnotation, E>(options);
|
|
44
|
+
|
|
45
|
+
const state = createSvelteImageAnnotatorState(opts);
|
|
46
|
+
|
|
47
|
+
const { hover, selection, store } = state;
|
|
48
|
+
|
|
49
|
+
const lifecycle = createLifecyleObserver<ImageAnnotation, E>(
|
|
50
|
+
store, selection, hover, undefined, opts.adapter, opts.autoSave);
|
|
51
|
+
|
|
52
|
+
let currentUser: User = createAnonymousGuest();
|
|
53
|
+
|
|
54
|
+
let style = opts.style;
|
|
55
|
+
|
|
56
|
+
// We'll wrap the image in a container DIV.
|
|
57
|
+
const container = document.createElement('DIV');
|
|
58
|
+
container.style.position = 'relative';
|
|
59
|
+
container.style.display = 'inline-block';
|
|
60
|
+
|
|
61
|
+
// Wrapper div has unwanted margin at the bottom otherwise!
|
|
62
|
+
img.style.display = 'block';
|
|
63
|
+
|
|
64
|
+
img.parentNode.insertBefore(container, img);
|
|
65
|
+
container.appendChild(img);
|
|
66
|
+
|
|
67
|
+
setTheme(img, container);
|
|
68
|
+
|
|
69
|
+
const annotationLayer = new SVGAnnotationLayer({
|
|
70
|
+
target: container,
|
|
71
|
+
props: {
|
|
72
|
+
drawingEnabled: opts.drawingEnabled,
|
|
73
|
+
image: img,
|
|
74
|
+
preferredDrawingMode: opts.drawingMode,
|
|
75
|
+
state,
|
|
76
|
+
style,
|
|
77
|
+
user: currentUser
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
annotationLayer.$on('click', (evt: CustomEvent<SVGAnnotationLayerPointerEvent>) => {
|
|
82
|
+
const { originalEvent, annotation } = evt.detail;
|
|
83
|
+
if (annotation)
|
|
84
|
+
selection.clickSelect(annotation.id, originalEvent);
|
|
85
|
+
else if (!selection.isEmpty())
|
|
86
|
+
selection.clear();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/*************************/
|
|
90
|
+
/* External API */
|
|
91
|
+
/******++++++*************/
|
|
92
|
+
|
|
93
|
+
// Most of the external API functions are covered in the base annotator
|
|
94
|
+
const base = createBaseAnnotator<ImageAnnotation, E>(store, opts.adapter);
|
|
95
|
+
|
|
96
|
+
const setStyle = (drawingStyle: DrawingStyle | ((annotation: ImageAnnotation) => DrawingStyle) | undefined) => {
|
|
97
|
+
style = drawingStyle;
|
|
98
|
+
annotationLayer.$set({ style });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const destroy = () => {
|
|
102
|
+
// Destroy Svelte annotation layer
|
|
103
|
+
annotationLayer.$destroy();
|
|
104
|
+
|
|
105
|
+
// Unwrap the image
|
|
106
|
+
container.parentNode.insertBefore(img, container);
|
|
107
|
+
container.parentNode.removeChild(container);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const getUser = () => currentUser;
|
|
111
|
+
|
|
112
|
+
const registerDrawingTool = (name: string, tool: typeof SvelteComponent, opts?: DrawingToolOpts) =>
|
|
113
|
+
registerTool(name, tool, opts);
|
|
114
|
+
|
|
115
|
+
const registerShapeEditor = (shapeType: ShapeType, editor: typeof SvelteComponent) =>
|
|
116
|
+
registerEditor(shapeType, editor);
|
|
117
|
+
|
|
118
|
+
const setDrawingTool = (t: DrawingTool) => {
|
|
119
|
+
const { tool, opts } = getTool(t);
|
|
120
|
+
// @ts-ignore
|
|
121
|
+
annotationLayer.$set({ tool, opts })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const setDrawingEnabled = (enabled: boolean) =>
|
|
125
|
+
annotationLayer.$set({ drawingEnabled: enabled });
|
|
126
|
+
|
|
127
|
+
const setSelected = (arg?: string | string[]) => {
|
|
128
|
+
if (arg) {
|
|
129
|
+
selection.setSelected(arg);
|
|
130
|
+
} else {
|
|
131
|
+
selection.clear();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const setUser = (user: User) => {
|
|
136
|
+
currentUser = user;
|
|
137
|
+
annotationLayer.$set({ user });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
...base,
|
|
142
|
+
get style() { return style },
|
|
143
|
+
set style(s: DrawingStyle | ((annotation: ImageAnnotation) => DrawingStyle) | undefined) { setStyle(s) },
|
|
144
|
+
destroy,
|
|
145
|
+
getUser,
|
|
146
|
+
listDrawingTools,
|
|
147
|
+
on: lifecycle.on,
|
|
148
|
+
off: lifecycle.off,
|
|
149
|
+
registerDrawingTool,
|
|
150
|
+
registerShapeEditor,
|
|
151
|
+
setDrawingEnabled,
|
|
152
|
+
setDrawingTool,
|
|
153
|
+
setSelected,
|
|
154
|
+
setUser,
|
|
155
|
+
state
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PointerSelectAction } from '@annotorious/core';
|
|
2
|
+
import type { Annotation, DrawingStyle, FormatAdapter } from '@annotorious/core';
|
|
3
|
+
import type { ImageAnnotation } from './model';
|
|
4
|
+
|
|
5
|
+
export interface AnnotoriousOpts<I extends Annotation = ImageAnnotation, E extends unknown = ImageAnnotation> {
|
|
6
|
+
|
|
7
|
+
adapter?: FormatAdapter<I, E>;
|
|
8
|
+
|
|
9
|
+
autoSave?: boolean;
|
|
10
|
+
|
|
11
|
+
drawingEnabled?: boolean;
|
|
12
|
+
|
|
13
|
+
// 'click': starts on single click, user cannot select unless drawingEnabled = false
|
|
14
|
+
// 'drag': starts drawing on drag, single click always selects
|
|
15
|
+
drawingMode?: DrawingMode;
|
|
16
|
+
|
|
17
|
+
pointerSelectAction?: PointerSelectAction | ((a: I) => PointerSelectAction);
|
|
18
|
+
|
|
19
|
+
style?: DrawingStyle | ((annotation: I) => DrawingStyle);
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type DrawingMode = 'click' | 'drag';
|
|
24
|
+
|
|
25
|
+
export const fillDefaults = <I extends ImageAnnotation = ImageAnnotation, E extends unknown = ImageAnnotation> (
|
|
26
|
+
opts: AnnotoriousOpts<I, E>
|
|
27
|
+
): AnnotoriousOpts<I, E> => {
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
...opts,
|
|
31
|
+
drawingEnabled: opts.drawingEnabled === undefined ? true : opts.drawingEnabled,
|
|
32
|
+
drawingMode: opts.drawingMode || 'drag',
|
|
33
|
+
pointerSelectAction: opts.pointerSelectAction || PointerSelectAction.EDIT
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<script type="ts">
|
|
2
|
+
import { onMount, type SvelteComponent } from 'svelte';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import type { DrawingStyle, StoreChangeEvent, User } from '@annotorious/core';
|
|
5
|
+
import { ShapeType } from '../model';
|
|
6
|
+
import type { ImageAnnotation, Shape} from '../model';
|
|
7
|
+
import { getEditor, EditorMount } from './editors';
|
|
8
|
+
import { Ellipse, Polygon, Rectangle} from './shapes';
|
|
9
|
+
import { getTool, ToolMount } from './tools';
|
|
10
|
+
import { enableResponsive } from './utils';
|
|
11
|
+
import { createSVGTransform } from './Transform';
|
|
12
|
+
import { addEventListeners } from './SVGAnnotationLayerPointerEvent';
|
|
13
|
+
import type { SvelteImageAnnotatorState } from 'src/state';
|
|
14
|
+
import type { DrawingMode } from 'src/AnnotoriousOpts';
|
|
15
|
+
|
|
16
|
+
/** Props **/
|
|
17
|
+
export let drawingEnabled: boolean;
|
|
18
|
+
export let image: HTMLImageElement | HTMLCanvasElement;
|
|
19
|
+
export let preferredDrawingMode: DrawingMode;
|
|
20
|
+
export let state: SvelteImageAnnotatorState;
|
|
21
|
+
export let style: DrawingStyle | ((annotation: ImageAnnotation) => DrawingStyle) = undefined;
|
|
22
|
+
export let { tool, opts } = getTool('rectangle');
|
|
23
|
+
export let user: User;
|
|
24
|
+
|
|
25
|
+
$: drawingMode = opts?.drawingMode || preferredDrawingMode;
|
|
26
|
+
|
|
27
|
+
/** Drawing tool layer **/
|
|
28
|
+
let drawingEl: SVGGElement;
|
|
29
|
+
|
|
30
|
+
/** Responsive scaling **/
|
|
31
|
+
let svgEl: SVGSVGElement;
|
|
32
|
+
|
|
33
|
+
let scale: ReturnType<typeof enableResponsive>;
|
|
34
|
+
|
|
35
|
+
onMount(() => scale = enableResponsive(image, svgEl));
|
|
36
|
+
|
|
37
|
+
$: transform = createSVGTransform(svgEl);
|
|
38
|
+
|
|
39
|
+
/** Selection tracking */
|
|
40
|
+
const { selection, store } = state;
|
|
41
|
+
|
|
42
|
+
$: ({ onPointerDown, onPointerUp } = addEventListeners(svgEl, store));
|
|
43
|
+
|
|
44
|
+
let storeObserver = null;
|
|
45
|
+
|
|
46
|
+
let editableAnnotations: ImageAnnotation[] = null;
|
|
47
|
+
|
|
48
|
+
$: isEditable = (a: ImageAnnotation) => $selection.selected.find(s => s.id === a.id && s.editable);
|
|
49
|
+
|
|
50
|
+
$: trackSelection($selection.selected);
|
|
51
|
+
|
|
52
|
+
const trackSelection = (selected: { id: string, editable?: boolean }[]) => {
|
|
53
|
+
store.unobserve(storeObserver);
|
|
54
|
+
|
|
55
|
+
// Track only editable annotations
|
|
56
|
+
const editableIds =
|
|
57
|
+
selected.filter(({ editable }) => editable).map(({ id }) => id);
|
|
58
|
+
|
|
59
|
+
if (editableIds.length > 0) {
|
|
60
|
+
// Resolve selected IDs from the store
|
|
61
|
+
editableAnnotations = editableIds.map(id => store.getAnnotation(id));
|
|
62
|
+
|
|
63
|
+
// Track updates on the editable annotations
|
|
64
|
+
storeObserver = (event: StoreChangeEvent<ImageAnnotation>) => {
|
|
65
|
+
const { updated } = event.changes;
|
|
66
|
+
editableAnnotations = updated.map(change => change.newValue);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
store.observe(storeObserver, { annotations: editableIds });
|
|
70
|
+
} else {
|
|
71
|
+
editableAnnotations = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const onSelectionCreated = <T extends Shape>(evt: CustomEvent<T>) => {
|
|
76
|
+
const id = uuidv4();
|
|
77
|
+
|
|
78
|
+
const annotation: ImageAnnotation = {
|
|
79
|
+
id,
|
|
80
|
+
bodies: [],
|
|
81
|
+
target: {
|
|
82
|
+
annotation: id,
|
|
83
|
+
selector: evt.detail,
|
|
84
|
+
creator: user,
|
|
85
|
+
created: new Date()
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
store.addAnnotation(annotation);
|
|
90
|
+
|
|
91
|
+
selection.setSelected(annotation.id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const onChangeSelected = (annotation: ImageAnnotation) => (event: CustomEvent<Shape>) => {
|
|
95
|
+
const { target } = annotation;
|
|
96
|
+
|
|
97
|
+
// We don't consider a shape edit an 'update' if it happens within 10mins
|
|
98
|
+
const GRACE_PERIOD = 10 * 60 * 1000;
|
|
99
|
+
|
|
100
|
+
const isUpdate =
|
|
101
|
+
target.creator?.id !== user.id ||
|
|
102
|
+
!target.created ||
|
|
103
|
+
new Date().getTime() - target.created.getTime() > GRACE_PERIOD;
|
|
104
|
+
|
|
105
|
+
store.updateTarget({
|
|
106
|
+
...target,
|
|
107
|
+
selector: event.detail,
|
|
108
|
+
created: isUpdate ? target.created : new Date(),
|
|
109
|
+
updated: isUpdate ? new Date() : null,
|
|
110
|
+
updatedBy: isUpdate ? user : null
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<svg
|
|
116
|
+
bind:this={svgEl}
|
|
117
|
+
class="a9s-annotationlayer"
|
|
118
|
+
class:drawing={tool}
|
|
119
|
+
on:pointerup={onPointerUp}
|
|
120
|
+
on:pointerdown={onPointerDown}>
|
|
121
|
+
|
|
122
|
+
<g>
|
|
123
|
+
{#each $store as annotation}
|
|
124
|
+
{#if !isEditable(annotation)}
|
|
125
|
+
{@const selector = annotation.target.selector}
|
|
126
|
+
{#key annotation.id}
|
|
127
|
+
{#if (selector.type === ShapeType.ELLIPSE)}
|
|
128
|
+
<Ellipse annotation={annotation} geom={selector.geometry} style={style} />
|
|
129
|
+
{:else if (selector.type === ShapeType.RECTANGLE)}
|
|
130
|
+
<Rectangle annotation={annotation} geom={selector.geometry} style={style} />
|
|
131
|
+
{:else if (selector.type === ShapeType.POLYGON)}
|
|
132
|
+
<Polygon annotation={annotation} geom={selector.geometry} style={style} />
|
|
133
|
+
{/if}
|
|
134
|
+
{/key}
|
|
135
|
+
{/if}
|
|
136
|
+
{/each}
|
|
137
|
+
</g>
|
|
138
|
+
|
|
139
|
+
<g
|
|
140
|
+
bind:this={drawingEl}
|
|
141
|
+
class="drawing" >
|
|
142
|
+
{#if drawingEl}
|
|
143
|
+
{#if editableAnnotations}
|
|
144
|
+
{#each editableAnnotations as editable}
|
|
145
|
+
{#key editable.id}
|
|
146
|
+
<EditorMount
|
|
147
|
+
target={drawingEl}
|
|
148
|
+
editor={getEditor(editable.target.selector)}
|
|
149
|
+
annotation={editable}
|
|
150
|
+
style={style}
|
|
151
|
+
transform={transform}
|
|
152
|
+
viewportScale={$scale}
|
|
153
|
+
on:change={onChangeSelected(editable)} />
|
|
154
|
+
{/key}
|
|
155
|
+
{/each}
|
|
156
|
+
{:else if (tool && drawingEnabled)}
|
|
157
|
+
{#key tool}
|
|
158
|
+
<ToolMount
|
|
159
|
+
target={drawingEl}
|
|
160
|
+
tool={tool}
|
|
161
|
+
drawingMode={drawingMode}
|
|
162
|
+
transform={transform}
|
|
163
|
+
viewportScale={$scale}
|
|
164
|
+
on:create={onSelectionCreated} />
|
|
165
|
+
{/key}
|
|
166
|
+
{/if}
|
|
167
|
+
{/if}
|
|
168
|
+
</g>
|
|
169
|
+
</svg>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createEventDispatcher } from 'svelte';
|
|
2
|
+
import type { SvelteImageAnnotationStore } from '../state';
|
|
3
|
+
import type { ImageAnnotation } from '../model';
|
|
4
|
+
import { isTouch } from './utils';
|
|
5
|
+
|
|
6
|
+
export interface SVGAnnotationLayerPointerEvent {
|
|
7
|
+
|
|
8
|
+
originalEvent: PointerEvent;
|
|
9
|
+
|
|
10
|
+
annotation?: ImageAnnotation;
|
|
11
|
+
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Maximum amount of ms between pointer down and up to make it a click
|
|
15
|
+
const MAX_CLICK_DURATION = 250;
|
|
16
|
+
|
|
17
|
+
export const addEventListeners = (svg: SVGSVGElement, store: SvelteImageAnnotationStore) => {
|
|
18
|
+
const dispatch = createEventDispatcher<{ click: SVGAnnotationLayerPointerEvent}>();
|
|
19
|
+
|
|
20
|
+
let lastPointerDown: number;
|
|
21
|
+
|
|
22
|
+
const onPointerDown = () =>
|
|
23
|
+
lastPointerDown = performance.now();
|
|
24
|
+
|
|
25
|
+
const onPointerUp = (evt: PointerEvent) => {
|
|
26
|
+
const duration = performance.now() - lastPointerDown;
|
|
27
|
+
|
|
28
|
+
if (duration < MAX_CLICK_DURATION) {
|
|
29
|
+
const { x, y } = getSVGPoint(evt, svg);
|
|
30
|
+
|
|
31
|
+
const annotation = store.getAt(x, y);
|
|
32
|
+
|
|
33
|
+
if (annotation)
|
|
34
|
+
dispatch('click', { originalEvent: evt, annotation });
|
|
35
|
+
else
|
|
36
|
+
dispatch('click', { originalEvent: evt });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { onPointerDown, onPointerUp };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const getSVGPoint = (evt: PointerEvent, svg: SVGSVGElement) => {
|
|
44
|
+
const pt = svg.createSVGPoint();
|
|
45
|
+
const bbox = svg.getBoundingClientRect();
|
|
46
|
+
|
|
47
|
+
const x = evt.clientX - bbox.x;
|
|
48
|
+
const y = evt.clientY - bbox.y;
|
|
49
|
+
|
|
50
|
+
const { left, top } = svg.getBoundingClientRect();
|
|
51
|
+
pt.x = x + left;
|
|
52
|
+
pt.y = y + top;
|
|
53
|
+
|
|
54
|
+
return pt.matrixTransform(svg.getScreenCTM().inverse());
|
|
55
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface Transform {
|
|
2
|
+
|
|
3
|
+
elementToImage: (offsetX: number, offsetY: number) => [ number, number ]
|
|
4
|
+
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const IdentityTransform: Transform = {
|
|
8
|
+
|
|
9
|
+
elementToImage: (offsetX: number, offsetY: number) => ([ offsetX, offsetY ])
|
|
10
|
+
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const createSVGTransform = (svg: SVGSVGElement): Transform => ({
|
|
14
|
+
|
|
15
|
+
elementToImage: (offsetX: number, offsetY: number) => {
|
|
16
|
+
const pt = svg.createSVGPoint();
|
|
17
|
+
pt.x = offsetX;
|
|
18
|
+
pt.y = offsetY;
|
|
19
|
+
|
|
20
|
+
const { x, y } = pt.matrixTransform(svg.getCTM().inverse());
|
|
21
|
+
return [x, y];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script type="ts">
|
|
2
|
+
import { createEventDispatcher } from 'svelte';
|
|
3
|
+
import type { Shape } from '../../model';
|
|
4
|
+
import type { Handle } from './Handle';
|
|
5
|
+
import type { Transform } from '../Transform';
|
|
6
|
+
|
|
7
|
+
const dispatch = createEventDispatcher<{ grab: undefined, release: undefined, change: Shape }>();
|
|
8
|
+
|
|
9
|
+
/** Props */
|
|
10
|
+
export let shape: Shape;
|
|
11
|
+
export let editor: (shape: Shape, handle: Handle, delta: [number, number]) => Shape;
|
|
12
|
+
export let transform: Transform;
|
|
13
|
+
|
|
14
|
+
let grabbedHandle: Handle = null;
|
|
15
|
+
|
|
16
|
+
let origin: [number, number];
|
|
17
|
+
|
|
18
|
+
let initialShape: Shape = null;
|
|
19
|
+
|
|
20
|
+
const onGrab = (handle: Handle) => (evt: PointerEvent) => {
|
|
21
|
+
grabbedHandle = handle;
|
|
22
|
+
origin = transform.elementToImage(evt.offsetX, evt.offsetY);
|
|
23
|
+
initialShape = shape;
|
|
24
|
+
|
|
25
|
+
const target = evt.target as Element;
|
|
26
|
+
target.setPointerCapture(evt.pointerId);
|
|
27
|
+
|
|
28
|
+
dispatch('grab');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const onPointerMove = (evt: PointerEvent) => {
|
|
32
|
+
if (grabbedHandle) {
|
|
33
|
+
const [x, y] = transform.elementToImage(evt.offsetX, evt.offsetY);
|
|
34
|
+
|
|
35
|
+
const delta: [number, number] = [x - origin[0], y - origin[1]];
|
|
36
|
+
|
|
37
|
+
shape = editor(initialShape, grabbedHandle, delta);
|
|
38
|
+
|
|
39
|
+
dispatch('change', shape);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const onRelease = (evt: PointerEvent) => {
|
|
44
|
+
const target = evt.target as Element;
|
|
45
|
+
target.releasePointerCapture(evt.pointerId);
|
|
46
|
+
|
|
47
|
+
grabbedHandle = null;
|
|
48
|
+
|
|
49
|
+
initialShape = shape;
|
|
50
|
+
|
|
51
|
+
dispatch('release');
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<g
|
|
56
|
+
class="a9s-annotation selected"
|
|
57
|
+
on:pointerup={onRelease}
|
|
58
|
+
on:pointermove={onPointerMove}>
|
|
59
|
+
|
|
60
|
+
<slot grab={onGrab} />
|
|
61
|
+
</g>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { DrawingStyle } from '@annotorious/core';
|
|
3
|
+
import { createEventDispatcher, onMount, type SvelteComponent } from 'svelte';
|
|
4
|
+
import type { ImageAnnotation, Shape } from '../../model';
|
|
5
|
+
import { computeStyle } from '../utils/styling';
|
|
6
|
+
import type { Transform } from '../Transform';
|
|
7
|
+
|
|
8
|
+
const dispatch = createEventDispatcher<{ grab: undefined, release: undefined, change: Shape }>();
|
|
9
|
+
|
|
10
|
+
/** Props */
|
|
11
|
+
export let annotation: ImageAnnotation;
|
|
12
|
+
export let editor: typeof SvelteComponent;
|
|
13
|
+
export let style: DrawingStyle | ((annotation: ImageAnnotation) => DrawingStyle) = undefined;
|
|
14
|
+
export let target: SVGGElement;
|
|
15
|
+
export let transform: Transform;
|
|
16
|
+
export let viewportScale: number;
|
|
17
|
+
|
|
18
|
+
let editorComponent: SvelteComponent;
|
|
19
|
+
|
|
20
|
+
$: computedStyle = computeStyle(annotation, style);
|
|
21
|
+
|
|
22
|
+
$: if (editorComponent) editorComponent.$set({ transform });
|
|
23
|
+
|
|
24
|
+
$: if (editorComponent) editorComponent.$set({ viewportScale });
|
|
25
|
+
|
|
26
|
+
onMount(() => {
|
|
27
|
+
editorComponent = new editor({
|
|
28
|
+
target,
|
|
29
|
+
props: { shape: annotation.target.selector, computedStyle, transform, viewportScale }
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
editorComponent.$on('change', event => {
|
|
33
|
+
editorComponent.$$set({ shape: event.detail });
|
|
34
|
+
dispatch('change', event.detail);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
editorComponent.$on('grab', event => dispatch('grab', event.detail));
|
|
38
|
+
editorComponent.$on('release', event => dispatch('release', event.detail));
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
editorComponent.$destroy();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Handle = string;
|
|
2
|
+
|
|
3
|
+
export const Handle = (value: string | number) => `HANDLE-${value}`;
|
|
4
|
+
|
|
5
|
+
Handle.SHAPE = 'SHAPE';
|
|
6
|
+
|
|
7
|
+
Handle.TOP = 'TOP';
|
|
8
|
+
|
|
9
|
+
Handle.RIGHT = 'RIGHT';
|
|
10
|
+
|
|
11
|
+
Handle.BOTTOM = 'BOTTOM';
|
|
12
|
+
|
|
13
|
+
Handle.LEFT = 'LEFT';
|
|
14
|
+
|
|
15
|
+
Handle.TOP_LEFT = 'TOP_LEFT';
|
|
16
|
+
|
|
17
|
+
Handle.TOP_RIGHT = 'TOP_RIGHT';
|
|
18
|
+
|
|
19
|
+
Handle.BOTTOM_RIGHT = 'BOTTOM_RIGHT';
|
|
20
|
+
|
|
21
|
+
Handle.BOTTOM_LEFT = 'BOTTOM_LEFT';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ShapeType, type Shape } from '../../model';
|
|
2
|
+
import type { SvelteComponent } from 'svelte';
|
|
3
|
+
import { PolygonEditor } from './polygon';
|
|
4
|
+
import { RectangleEditor } from './rectangle';
|
|
5
|
+
|
|
6
|
+
const REGISTERED = new Map<ShapeType, typeof SvelteComponent>([
|
|
7
|
+
[ShapeType.RECTANGLE, RectangleEditor],
|
|
8
|
+
[ShapeType.POLYGON, PolygonEditor]
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export const getEditor = (shape: Shape) => REGISTERED.get(shape.type);
|
|
12
|
+
|
|
13
|
+
export const registerEditor = (shapeType: ShapeType, editor: typeof SvelteComponent) =>
|
|
14
|
+
REGISTERED.set(shapeType, editor);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script type="ts">
|
|
2
|
+
import { boundsFromPoints } from '../../../model';
|
|
3
|
+
import type { Polygon } from '../../../model';
|
|
4
|
+
import type { Transform } from '../../Transform';
|
|
5
|
+
import { Editor, Handle } from '..';
|
|
6
|
+
|
|
7
|
+
/** Props */
|
|
8
|
+
export let shape: Polygon;
|
|
9
|
+
export let computedStyle: string = undefined;
|
|
10
|
+
export let transform: Transform;
|
|
11
|
+
export let viewportScale: number = 1;
|
|
12
|
+
|
|
13
|
+
$: geom = shape.geometry;
|
|
14
|
+
|
|
15
|
+
$: handleSize = 10 / viewportScale;
|
|
16
|
+
|
|
17
|
+
const editor = (polygon: Polygon, handle: Handle, delta: [number, number]) => {
|
|
18
|
+
let points: [number, number][];
|
|
19
|
+
|
|
20
|
+
if (handle === Handle.SHAPE) {
|
|
21
|
+
points = polygon.geometry.points.map(([x, y]) => [x + delta[0], y + delta[1]]);
|
|
22
|
+
} else {
|
|
23
|
+
points = polygon.geometry.points.map(([x, y], idx) =>
|
|
24
|
+
handle === Handle(idx) ? [x + delta[0], y + delta[1]] : [x, y]
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const bounds = boundsFromPoints(points);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
...polygon,
|
|
32
|
+
geometry: { points, bounds }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<Editor
|
|
38
|
+
shape={shape}
|
|
39
|
+
transform={transform}
|
|
40
|
+
editor={editor}
|
|
41
|
+
on:change
|
|
42
|
+
on:grab
|
|
43
|
+
on:release
|
|
44
|
+
let:grab={grab}>
|
|
45
|
+
|
|
46
|
+
<polygon
|
|
47
|
+
class="a9s-outer"
|
|
48
|
+
style={computedStyle ? 'display:none;' : undefined}
|
|
49
|
+
on:pointerdown={grab(Handle.SHAPE)}
|
|
50
|
+
points={geom.points.map(xy => xy.join(',')).join(' ')} />
|
|
51
|
+
|
|
52
|
+
<polygon
|
|
53
|
+
class="a9s-inner a9s-shape-handle"
|
|
54
|
+
style={computedStyle}
|
|
55
|
+
on:pointerdown={grab(Handle.SHAPE)}
|
|
56
|
+
points={geom.points.map(xy => xy.join(',')).join(' ')} />
|
|
57
|
+
|
|
58
|
+
{#each geom.points as point, idx}
|
|
59
|
+
<rect
|
|
60
|
+
class="a9s-corner-handle"
|
|
61
|
+
on:pointerdown={grab(Handle(idx))}
|
|
62
|
+
x={point[0] - handleSize / 2} y={point[1] - handleSize / 2} height={handleSize} width={handleSize} />
|
|
63
|
+
{/each}
|
|
64
|
+
</Editor>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as PolygonEditor } from './PolygonEditor.svelte';
|