@annotorious/annotorious 3.3.6 → 3.4.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annotorious/annotorious",
3
- "version": "3.3.6",
3
+ "version": "3.4.0",
4
4
  "description": "Add image annotation functionality to any web page with a few lines of JavaScript",
5
5
  "author": "Rainer Simon",
6
6
  "license": "BSD-3-Clause",
@@ -41,15 +41,15 @@
41
41
  "@tsconfig/svelte": "^5.0.4",
42
42
  "@types/rbush": "^4.0.0",
43
43
  "jsdom": "^26.1.0",
44
- "svelte": "^4.2.19",
44
+ "svelte": "^4.2.20",
45
45
  "svelte-preprocess": "^6.0.3",
46
46
  "typescript": "5.8.3",
47
- "vite": "^5.4.18",
48
- "vite-plugin-dts": "^4.5.3",
49
- "vitest": "^3.1.2"
47
+ "vite": "^5.4.19",
48
+ "vite-plugin-dts": "^4.5.4",
49
+ "vitest": "^3.1.4"
50
50
  },
51
51
  "dependencies": {
52
- "@annotorious/core": "3.3.6",
52
+ "@annotorious/core": "3.4.0",
53
53
  "rbush": "^4.0.1",
54
54
  "svg-pathdata": "^7.2.0",
55
55
  "uuid": "^11.1.0"
@@ -50,6 +50,10 @@
50
50
  cursor: move;
51
51
  }
52
52
 
53
+ .a9s-handle.a9s-corner-handle {
54
+ cursor: crosshair;
55
+ }
56
+
53
57
  .a9s-edge-handle-top {
54
58
  cursor: n-resize;
55
59
  }
@@ -143,16 +143,19 @@
143
143
  const getEditor = (shape: Shape): typeof SvelteComponent => _getEditor(shape)!;
144
144
  </script>
145
145
 
146
+ <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
146
147
  <svg
147
148
  bind:this={svgEl}
149
+ role="application"
150
+ tabindex={0}
148
151
  class="a9s-annotationlayer"
149
152
  class:drawing={tool}
153
+ class:editing={editableAnnotations}
150
154
  class:hidden={!visible}
151
155
  class:hover={$hover}
152
156
  on:pointerup={onPointerUp}
153
157
  on:pointerdown={onPointerDown}
154
158
  on:pointermove={onPointerMove}>
155
-
156
159
  <g>
157
160
  {#each $store.filter(a => isImageAnnotation(a)) as annotation}
158
161
  {#if isImageAnnotation(annotation) && !isEditable(annotation)}
@@ -9,6 +9,7 @@
9
9
  export let shape: Shape;
10
10
  export let editor: (shape: Shape, handle: string, delta: [number, number]) => Shape;
11
11
  export let transform: Transform;
12
+ export let svgEl: SVGSVGElement;
12
13
 
13
14
  let grabbedHandle: string | undefined;
14
15
 
@@ -18,7 +19,22 @@
18
19
 
19
20
  const onGrab = (handle: string) => (evt: PointerEvent) => {
20
21
  grabbedHandle = handle;
21
- origin = transform.elementToImage(evt.offsetX, evt.offsetY);
22
+
23
+ // For legacy compatibility: offsetX and offsetY are not
24
+ // available on synthetic events, currently used by the
25
+ // new PolygonEditor. Old versions of Annotorious, however,
26
+ // won't yet forward the svgEl prop!
27
+ if (svgEl) {
28
+ const { left, top } = svgEl.getBoundingClientRect();
29
+ const offsetX = evt.clientX - left;
30
+ const offsetY = evt.clientY - top;
31
+
32
+ origin = transform.elementToImage(offsetX, offsetY);
33
+ } else {
34
+ const { offsetX, offsetY } = evt;
35
+ origin = transform.elementToImage(offsetX, offsetY);
36
+ }
37
+
22
38
  initialShape = shape;
23
39
 
24
40
  const target = evt.target as Element;
@@ -27,7 +27,13 @@
27
27
  onMount(() => {
28
28
  editorComponent = new editor({
29
29
  target,
30
- props: { shape: annotation.target.selector, computedStyle, transform, viewportScale }
30
+ props: {
31
+ shape: annotation.target.selector,
32
+ computedStyle,
33
+ transform,
34
+ viewportScale,
35
+ svgEl: target.closest('svg')
36
+ }
31
37
  });
32
38
 
33
39
  editorComponent.$on('change', event => {
@@ -5,62 +5,90 @@
5
5
  export let x: number;
6
6
  export let y: number;
7
7
  export let scale: number;
8
- export let radius: number = 30;
8
+ export let selected: Boolean | undefined = undefined;
9
9
 
10
10
  let touched = false;
11
11
 
12
- const onPointerDown = (event: PointerEvent) => {
13
- if (event.pointerType === 'touch')
12
+ const onPointerDown = (evt: PointerEvent) => {
13
+ if (evt.pointerType === 'touch')
14
14
  touched = true;
15
15
  }
16
16
 
17
17
  const onPointerUp = () =>
18
18
  touched = false;
19
19
 
20
- $: handleSize = 10 / scale;
20
+ $: handleRadius = 4 / scale;
21
21
  </script>
22
22
 
23
23
  {#if isTouch}
24
- <g class="a9s-touch-handle">
24
+ <circle
25
+ cx={x}
26
+ cy={y}
27
+ r={2 * handleRadius} />
28
+ {:else}
29
+ <g class={`a9s-handle ${$$props.class || ''}`.trim()}>
25
30
  <circle
26
- cx={x}
27
- cy={y}
28
- r={radius / scale}
29
- class="a9s-touch-halo"
30
- class:touched={touched}
31
+ class="a9s-handle-buffer"
32
+ cx={x}
33
+ cy={y}
34
+ r={handleRadius + (6 / scale)}
35
+ on:pointerenter
36
+ on:pointerleave
31
37
  on:pointerdown
32
- on:pointerdown={onPointerDown}
38
+ on:pointerdown={onPointerDown}
39
+ on:pointerup
33
40
  on:pointerup={onPointerUp} />
34
41
 
35
- <rect
36
- class={`a9s-handle ${$$props.class || ''}`.trim()}
37
- x={x - handleSize / 2}
38
- y={y - handleSize / 2}
39
- width={handleSize}
40
- height={handleSize}
41
- on:pointerdown
42
- on:pointerdown={onPointerDown}
43
- on:pointerup={onPointerUp} />
42
+ {#if selected}
43
+ <circle
44
+ class="a9s-handle-selected"
45
+ cx={x}
46
+ cy={y}
47
+ r={handleRadius + (6 / scale)} />
48
+ {/if}
49
+
50
+ <circle
51
+ class={`a9s-handle-dot${selected ? ' selected': ''}`}
52
+ cx={x}
53
+ cy={y}
54
+ r={handleRadius} />
44
55
  </g>
45
- {:else}
46
- <rect
47
- class={`a9s-handle ${$$props.class || ''}`.trim()}
48
- x={x - handleSize / 2}
49
- y={y - handleSize / 2}
50
- width={handleSize}
51
- height={handleSize}
52
- on:pointerdown />
53
56
  {/if}
54
57
 
55
58
  <style>
56
- .a9s-touch-halo {
59
+ .a9s-handle-buffer {
57
60
  fill: transparent;
58
- stroke-width: 0;
59
61
  }
60
62
 
61
- .a9s-touch-halo.touched {
62
- fill: rgba(255, 255, 255, 0.25);
63
+ .a9s-handle-dot {
64
+ fill: #fff;
65
+ pointer-events: none;
66
+ stroke: rgba(0, 0, 0, 0.35);
67
+ stroke-width: 1px;
68
+ vector-effect: non-scaling-stroke;
63
69
  }
64
70
 
71
+ .a9s-handle-dot.selected {
72
+ fill: #1a1a1a;
73
+ stroke: none;
74
+ }
65
75
 
66
- </style>
76
+ .a9s-handle-selected {
77
+ animation: dash-rotate 350ms linear infinite reverse;
78
+ fill: rgba(255, 255, 255, 0.25);
79
+ stroke: rgba(0, 0, 0, 0.9);
80
+ stroke-dasharray: 2 2;
81
+ stroke-width: 1px;
82
+ pointer-events: none;
83
+ vector-effect: non-scaling-stroke;
84
+ }
85
+
86
+ @keyframes dash-rotate {
87
+ 0% {
88
+ stroke-dashoffset: 0;
89
+ }
90
+ 100% {
91
+ stroke-dashoffset: 4; /* Sum of dash + gap */
92
+ }
93
+ }
94
+ </style>
@@ -11,6 +11,7 @@
11
11
  multipolygonElementToPath
12
12
  } from '../../../model';
13
13
  import type { Transform } from '../../Transform';
14
+ import { getMaskDimensions } from '../../utils';
14
15
  import { Editor, Handle } from '..';
15
16
 
16
17
  /** Props */
@@ -18,6 +19,7 @@
18
19
  export let computedStyle: string | undefined;
19
20
  export let transform: Transform;
20
21
  export let viewportScale: number = 1;
22
+ export let svgEl: SVGSVGElement;
21
23
 
22
24
  $: geom = shape.geometry;
23
25
 
@@ -76,22 +78,33 @@
76
78
  }
77
79
  } as MultiPolygon;
78
80
  }
81
+
82
+ $: mask = getMaskDimensions(geom.bounds, 2 / viewportScale);
83
+
84
+ const maskId = `multipoly-mask-${Math.random().toString(36).substring(2, 12)}`;
79
85
  </script>
80
86
 
81
87
  <Editor
82
88
  shape={shape}
83
89
  transform={transform}
84
90
  editor={editor}
91
+ svgEl={svgEl}
85
92
  on:change
86
93
  on:grab
87
94
  on:release
88
95
  let:grab={grab}>
89
-
90
96
  {#each geom.polygons as element, elementIdx}
91
97
  <g>
98
+ <defs>
99
+ <mask id={`${maskId}-${elementIdx}`} class="a9s-multipolygon-editor-mask">
100
+ <rect x={mask.x} y={mask.y} width={mask.w} height={mask.h} />
101
+ <path d={multipolygonElementToPath(element)} />
102
+ </mask>
103
+ </defs>
104
+
92
105
  <path
93
106
  class="a9s-outer"
94
- style={computedStyle ? 'display:none;' : undefined}
107
+ mask={`url(#${maskId}-${elementIdx})`}
95
108
  fill-rule="evenodd"
96
109
  on:pointerdown={grab('SHAPE')}
97
110
  d={multipolygonElementToPath(element)} />
@@ -106,6 +119,7 @@
106
119
  {#each element.rings as ring, ringIdx}
107
120
  {#each ring.points as point, pointIdx}
108
121
  <Handle
122
+ class="a9s-corner-handle"
109
123
  on:pointerdown={grab(`HANDLE-${elementIdx}-${ringIdx}-${pointIdx}`)}
110
124
  x={point[0]} y={point[1]}
111
125
  scale={viewportScale} />
@@ -113,4 +127,14 @@
113
127
  {/each}
114
128
  </g>
115
129
  {/each}
116
- </Editor>
130
+ </Editor>
131
+
132
+ <style>
133
+ mask.a9s-multipolygon-editor-mask > rect {
134
+ fill: #fff;
135
+ }
136
+
137
+ mask.a9s-multipolygon-editor-mask > path {
138
+ fill: #000;
139
+ }
140
+ </style>
@@ -0,0 +1,76 @@
1
+ <script lang="ts">
2
+ import { isTouch } from '../../utils';
3
+
4
+ /** props **/
5
+ export let x: number;
6
+ export let y: number;
7
+ export let scale: number;
8
+
9
+ let touched = false;
10
+
11
+ const onPointerDown = (evt: PointerEvent) => {
12
+ if (evt.pointerType === 'touch')
13
+ touched = true;
14
+ }
15
+
16
+ const onPointerUp = () =>
17
+ touched = false;
18
+
19
+ $: handleRadius = 4 / scale;
20
+ </script>
21
+
22
+ {#if isTouch}
23
+ <circle
24
+ cx={x}
25
+ cy={y}
26
+ r={handleRadius} />
27
+ {:else}
28
+ <g class="a9s-polygon-midpoint">
29
+ <circle
30
+ class="a9s-polygon-midpoint-buffer"
31
+ cx={x}
32
+ cy={y}
33
+ r={1.75 * handleRadius}
34
+ on:pointerdown
35
+ on:pointerdown={onPointerDown} />
36
+
37
+ <circle
38
+ class="a9s-polygon-midpoint-outer"
39
+ cx={x}
40
+ cy={y}
41
+ r={handleRadius} />
42
+
43
+ <circle
44
+ class="a9s-polygon-midpoint-inner"
45
+ cx={x}
46
+ cy={y}
47
+ r={handleRadius} />
48
+ </g>
49
+ {/if}
50
+
51
+ <style>
52
+ .a9s-polygon-midpoint {
53
+ cursor: crosshair;
54
+ }
55
+
56
+ .a9s-polygon-midpoint-buffer {
57
+ fill: transparent;
58
+ }
59
+
60
+ .a9s-polygon-midpoint-outer {
61
+ display: none;
62
+ fill: transparent;
63
+ pointer-events: none;
64
+ stroke: rgba(0, 0, 0, 0.35);
65
+ stroke-width: 1.5px;
66
+ vector-effect: non-scaling-stroke;
67
+ }
68
+
69
+ .a9s-polygon-midpoint-inner {
70
+ fill: rgba(0, 0, 0, 0.25);
71
+ pointer-events: none;
72
+ stroke: #fff;
73
+ stroke-width: 1px;
74
+ vector-effect: non-scaling-stroke;
75
+ }
76
+ </style>