annotate-image 2.0.0-beta.1 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core.js +118 -38
- package/dist/core.min.js +1 -1
- package/dist/css/annotate.min.css +1 -1
- package/dist/jquery.js +117 -38
- package/dist/jquery.min.js +1 -1
- package/dist/react.js +119 -39
- package/dist/types/annotate-image.d.ts +42 -0
- package/dist/types/annotate-view.d.ts +1 -1
- package/dist/types/react.d.ts +2 -0
- package/dist/types/types.d.ts +2 -0
- package/dist/types/vue.d.ts +11 -0
- package/dist/vue.js +121 -39
- package/package.json +1 -1
- package/readme.md +25 -0
package/dist/vue.js
CHANGED
|
@@ -30,10 +30,11 @@ var AnnotateEdit = class {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
this.area = image.editOverlay.querySelector(".image-annotate-edit-area");
|
|
33
|
-
|
|
34
|
-
this.area.style.
|
|
35
|
-
this.area.style.
|
|
36
|
-
this.area.style.
|
|
33
|
+
const rendered = image.toRendered(this.note);
|
|
34
|
+
this.area.style.height = rendered.height + "px";
|
|
35
|
+
this.area.style.width = rendered.width + "px";
|
|
36
|
+
this.area.style.left = rendered.left + "px";
|
|
37
|
+
this.area.style.top = rendered.top + "px";
|
|
37
38
|
this.form = document.createElement("div");
|
|
38
39
|
this.form.className = "image-annotate-edit-form";
|
|
39
40
|
const formEl = document.createElement("form");
|
|
@@ -120,13 +121,20 @@ var AnnotateEdit = class {
|
|
|
120
121
|
}
|
|
121
122
|
this.image.notifySave(stripInternals(this.note));
|
|
122
123
|
this.destroy();
|
|
124
|
+
this.image.flushPendingRescale();
|
|
123
125
|
};
|
|
124
126
|
const pos = readInlinePosition(this.area);
|
|
125
127
|
const size = readInlineSize(this.area);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
const natural = this.image.toNatural({
|
|
129
|
+
top: pos.top,
|
|
130
|
+
left: pos.left,
|
|
131
|
+
width: size.width,
|
|
132
|
+
height: size.height
|
|
133
|
+
});
|
|
134
|
+
this.note.top = natural.top;
|
|
135
|
+
this.note.left = natural.left;
|
|
136
|
+
this.note.width = natural.width;
|
|
137
|
+
this.note.height = natural.height;
|
|
130
138
|
this.note.text = text;
|
|
131
139
|
if (this.image.api.save) {
|
|
132
140
|
this.busy = true;
|
|
@@ -160,6 +168,7 @@ var AnnotateEdit = class {
|
|
|
160
168
|
const idx = this.image.notes.indexOf(this.note);
|
|
161
169
|
if (idx !== -1) this.image.notes.splice(idx, 1);
|
|
162
170
|
this.image.notifyDelete(stripInternals(this.note));
|
|
171
|
+
this.image.flushPendingRescale();
|
|
163
172
|
};
|
|
164
173
|
if (this.image.api.delete) {
|
|
165
174
|
this.busy = true;
|
|
@@ -235,29 +244,29 @@ var AnnotateView = class {
|
|
|
235
244
|
});
|
|
236
245
|
}
|
|
237
246
|
}
|
|
238
|
-
/** Apply the note's position and dimensions to the area element. */
|
|
247
|
+
/** Apply the note's position and dimensions to the area element, scaled to rendered size. */
|
|
239
248
|
setPosition() {
|
|
249
|
+
const rendered = this.image.toRendered(this.note);
|
|
240
250
|
const innerDiv = this.area.firstElementChild;
|
|
241
|
-
innerDiv.style.height =
|
|
242
|
-
innerDiv.style.width =
|
|
243
|
-
this.area.style.left =
|
|
244
|
-
this.area.style.top =
|
|
251
|
+
innerDiv.style.height = rendered.height + "px";
|
|
252
|
+
innerDiv.style.width = rendered.width + "px";
|
|
253
|
+
this.area.style.left = rendered.left + "px";
|
|
254
|
+
this.area.style.top = rendered.top + "px";
|
|
245
255
|
}
|
|
246
256
|
/** Update the view's position, size, and text from the edit area after a save. */
|
|
247
257
|
resetPosition(editable, text) {
|
|
248
258
|
this.tooltip.textContent = text;
|
|
249
259
|
this.tooltip.style.display = "none";
|
|
250
|
-
const
|
|
251
|
-
const areaSize = readInlineSize(editable.area);
|
|
260
|
+
const rendered = this.image.toRendered(editable.note);
|
|
252
261
|
const innerDiv = this.area.firstElementChild;
|
|
253
|
-
innerDiv.style.height =
|
|
254
|
-
innerDiv.style.width =
|
|
255
|
-
this.area.style.left =
|
|
256
|
-
this.area.style.top =
|
|
257
|
-
this.note.top =
|
|
258
|
-
this.note.left =
|
|
259
|
-
this.note.height =
|
|
260
|
-
this.note.width =
|
|
262
|
+
innerDiv.style.height = rendered.height + "px";
|
|
263
|
+
innerDiv.style.width = rendered.width + "px";
|
|
264
|
+
this.area.style.left = rendered.left + "px";
|
|
265
|
+
this.area.style.top = rendered.top + "px";
|
|
266
|
+
this.note.top = editable.note.top;
|
|
267
|
+
this.note.left = editable.note.left;
|
|
268
|
+
this.note.height = editable.note.height;
|
|
269
|
+
this.note.width = editable.note.width;
|
|
261
270
|
this.note.text = text;
|
|
262
271
|
this.note.id = editable.note.id;
|
|
263
272
|
this.editable = true;
|
|
@@ -527,15 +536,25 @@ var AnnotateImage = class {
|
|
|
527
536
|
this._mode = "view";
|
|
528
537
|
this.activeEdit = null;
|
|
529
538
|
this.destroyed = false;
|
|
539
|
+
this.pendingRescale = false;
|
|
540
|
+
this.originalParent = null;
|
|
541
|
+
this.originalNextSibling = null;
|
|
530
542
|
this.options = options;
|
|
531
543
|
this.handlers = createDefaultHandlers();
|
|
532
544
|
this.img = img;
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
545
|
+
this.naturalWidth = img.naturalWidth || img.width;
|
|
546
|
+
this.naturalHeight = img.naturalHeight || img.height;
|
|
547
|
+
const rendered = img.getBoundingClientRect();
|
|
548
|
+
const renderedWidth = rendered.width || img.width;
|
|
549
|
+
const renderedHeight = rendered.height || img.height;
|
|
550
|
+
if (this.naturalWidth === 0 || this.naturalHeight === 0) {
|
|
536
551
|
throw new Error("image-annotate: image must have non-zero dimensions (is the image loaded?)");
|
|
537
552
|
}
|
|
553
|
+
this.scaleX = renderedWidth / this.naturalWidth;
|
|
554
|
+
this.scaleY = renderedHeight / this.naturalHeight;
|
|
538
555
|
this.notes = options.notes.map((n) => ({ ...n }));
|
|
556
|
+
this.originalParent = img.parentNode;
|
|
557
|
+
this.originalNextSibling = img.nextSibling;
|
|
539
558
|
this.canvas = document.createElement("div");
|
|
540
559
|
this.canvas.className = "image-annotate-canvas";
|
|
541
560
|
this.viewOverlay = document.createElement("div");
|
|
@@ -546,19 +565,13 @@ var AnnotateImage = class {
|
|
|
546
565
|
const editArea = document.createElement("div");
|
|
547
566
|
editArea.className = "image-annotate-edit-area";
|
|
548
567
|
this.editOverlay.appendChild(editArea);
|
|
549
|
-
this.canvas.appendChild(this.viewOverlay);
|
|
550
|
-
this.canvas.appendChild(this.editOverlay);
|
|
551
568
|
if (!img.parentNode) {
|
|
552
569
|
throw new Error("image-annotate: image must be in the DOM before initialization");
|
|
553
570
|
}
|
|
554
|
-
img.parentNode.insertBefore(this.canvas, img
|
|
555
|
-
this.canvas.
|
|
556
|
-
this.canvas.
|
|
557
|
-
this.canvas.
|
|
558
|
-
this.viewOverlay.style.height = height + "px";
|
|
559
|
-
this.viewOverlay.style.width = width + "px";
|
|
560
|
-
this.editOverlay.style.height = height + "px";
|
|
561
|
-
this.editOverlay.style.width = width + "px";
|
|
571
|
+
img.parentNode.insertBefore(this.canvas, img);
|
|
572
|
+
this.canvas.appendChild(img);
|
|
573
|
+
this.canvas.appendChild(this.viewOverlay);
|
|
574
|
+
this.canvas.appendChild(this.editOverlay);
|
|
562
575
|
this.api = this.options.api ? normalizeApi(this.options.api) : {};
|
|
563
576
|
if (this.api.load) {
|
|
564
577
|
this.loadFromApi();
|
|
@@ -568,7 +581,38 @@ var AnnotateImage = class {
|
|
|
568
581
|
if (this.options.editable) {
|
|
569
582
|
this.createButton();
|
|
570
583
|
}
|
|
571
|
-
|
|
584
|
+
if (options.autoResize !== false && typeof ResizeObserver !== "undefined") {
|
|
585
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
586
|
+
const entry = entries[0];
|
|
587
|
+
if (!entry) return;
|
|
588
|
+
const { width, height } = entry.contentRect;
|
|
589
|
+
if (width === 0 || height === 0) return;
|
|
590
|
+
this.rescale(width, height);
|
|
591
|
+
});
|
|
592
|
+
this.resizeObserver.observe(this.canvas);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/** Convert a rect from natural image coordinates to rendered (scaled) coordinates. */
|
|
596
|
+
toRendered(rect) {
|
|
597
|
+
return {
|
|
598
|
+
top: rect.top * this.scaleY,
|
|
599
|
+
left: rect.left * this.scaleX,
|
|
600
|
+
width: rect.width * this.scaleX,
|
|
601
|
+
height: rect.height * this.scaleY
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/** Convert a rect from rendered (scaled) coordinates to natural image coordinates. */
|
|
605
|
+
toNatural(rect) {
|
|
606
|
+
const result = {
|
|
607
|
+
top: rect.top / this.scaleY,
|
|
608
|
+
left: rect.left / this.scaleX,
|
|
609
|
+
width: rect.width / this.scaleX,
|
|
610
|
+
height: rect.height / this.scaleY
|
|
611
|
+
};
|
|
612
|
+
if (!isFinite(result.top) || !isFinite(result.left) || !isFinite(result.width) || !isFinite(result.height)) {
|
|
613
|
+
throw new Error("image-annotate: scale conversion produced non-finite coordinates");
|
|
614
|
+
}
|
|
615
|
+
return result;
|
|
572
616
|
}
|
|
573
617
|
/** Current interaction mode — 'view' for browsing, 'edit' when an annotation is being created or modified. */
|
|
574
618
|
get mode() {
|
|
@@ -640,8 +684,15 @@ var AnnotateImage = class {
|
|
|
640
684
|
if (this.button) {
|
|
641
685
|
this.button.remove();
|
|
642
686
|
}
|
|
687
|
+
if (this.resizeObserver) {
|
|
688
|
+
this.resizeObserver.disconnect();
|
|
689
|
+
this.resizeObserver = void 0;
|
|
690
|
+
}
|
|
691
|
+
if (this.originalParent && this.originalParent.isConnected) {
|
|
692
|
+
const ref2 = this.originalNextSibling?.parentNode === this.originalParent ? this.originalNextSibling : null;
|
|
693
|
+
this.originalParent.insertBefore(this.img, ref2);
|
|
694
|
+
}
|
|
643
695
|
this.canvas.remove();
|
|
644
|
-
this.img.style.display = "";
|
|
645
696
|
}
|
|
646
697
|
/** Cancel the active edit (if any) and return to view mode. */
|
|
647
698
|
cancelEdit() {
|
|
@@ -649,6 +700,34 @@ var AnnotateImage = class {
|
|
|
649
700
|
this.activeEdit.destroy();
|
|
650
701
|
this.setMode("view");
|
|
651
702
|
}
|
|
703
|
+
this.flushPendingRescale();
|
|
704
|
+
}
|
|
705
|
+
/** Recompute scale factors, deferring if an edit is active. */
|
|
706
|
+
rescale(renderedWidth, renderedHeight) {
|
|
707
|
+
if (this.mode === "edit") {
|
|
708
|
+
this.pendingRescale = true;
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
this.applyRescale(renderedWidth, renderedHeight);
|
|
712
|
+
}
|
|
713
|
+
/** Apply new scale factors and re-render all views. */
|
|
714
|
+
applyRescale(renderedWidth, renderedHeight) {
|
|
715
|
+
const newScaleX = renderedWidth / this.naturalWidth;
|
|
716
|
+
const newScaleY = renderedHeight / this.naturalHeight;
|
|
717
|
+
if (newScaleX === this.scaleX && newScaleY === this.scaleY) return;
|
|
718
|
+
this.scaleX = newScaleX;
|
|
719
|
+
this.scaleY = newScaleY;
|
|
720
|
+
this.destroyViews();
|
|
721
|
+
this.createViews();
|
|
722
|
+
}
|
|
723
|
+
/** @internal Flush any deferred rescale after an edit completes. */
|
|
724
|
+
flushPendingRescale() {
|
|
725
|
+
if (!this.pendingRescale) return;
|
|
726
|
+
this.pendingRescale = false;
|
|
727
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
728
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
729
|
+
this.applyRescale(rect.width, rect.height);
|
|
730
|
+
}
|
|
652
731
|
}
|
|
653
732
|
/** Replace all annotations with new data. Does not fire lifecycle callbacks. */
|
|
654
733
|
setNotes(notes) {
|
|
@@ -731,7 +810,9 @@ var AnnotateImage2 = defineComponent({
|
|
|
731
810
|
/** Annotations to render. */
|
|
732
811
|
notes: { type: Array },
|
|
733
812
|
/** Enable annotation editing. Default: true. */
|
|
734
|
-
editable: { type: Boolean, default: true }
|
|
813
|
+
editable: { type: Boolean, default: true },
|
|
814
|
+
/** Enable automatic re-scaling when the container resizes. Default: true. */
|
|
815
|
+
autoResize: { type: Boolean, default: true }
|
|
735
816
|
},
|
|
736
817
|
emits: {
|
|
737
818
|
save: (_note) => true,
|
|
@@ -749,6 +830,7 @@ var AnnotateImage2 = defineComponent({
|
|
|
749
830
|
try {
|
|
750
831
|
const instance = new AnnotateImage(imgRef.value, {
|
|
751
832
|
editable: props.editable,
|
|
833
|
+
autoResize: props.autoResize,
|
|
752
834
|
notes: props.notes ? props.notes.slice() : [],
|
|
753
835
|
onChange: (notes) => {
|
|
754
836
|
currentNotes.value = notes;
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -171,6 +171,7 @@ Creates an annotation layer on an image element. Returns an `AnnotateImage` inst
|
|
|
171
171
|
| `onDelete` | `(note: NoteData) => void` | — | Called after a note is deleted |
|
|
172
172
|
| `onLoad` | `(notes: NoteData[]) => void` | — | Called after notes are loaded |
|
|
173
173
|
| `onError` | `(ctx: AnnotateErrorContext) => void` | — | Called on API errors (defaults to `console.error`) |
|
|
174
|
+
| `autoResize` | `boolean` | `true` | Re-scale annotations when the container resizes |
|
|
174
175
|
|
|
175
176
|
#### `AnnotateApi`
|
|
176
177
|
|
|
@@ -228,6 +229,7 @@ type NoteData = Omit<AnnotationNote, 'view' | 'editable'>;
|
|
|
228
229
|
| `onDelete` | `(note: NoteData) => void` | — | Note deleted |
|
|
229
230
|
| `onLoad` | `(notes: NoteData[]) => void` | — | Notes loaded |
|
|
230
231
|
| `onError` | `(ctx: AnnotateErrorContext) => void` | — | Error occurred |
|
|
232
|
+
| `autoResize` | `boolean` | `true` | Re-scale on container resize |
|
|
231
233
|
|
|
232
234
|
#### Ref Methods (`AnnotateImageRef`)
|
|
233
235
|
|
|
@@ -249,6 +251,7 @@ type NoteData = Omit<AnnotationNote, 'view' | 'editable'>;
|
|
|
249
251
|
| `height` | `Number` | — | Image height in pixels |
|
|
250
252
|
| `notes` | `AnnotationNote[]` | — | Initial annotations |
|
|
251
253
|
| `editable` | `Boolean` | `true` | Enable editing |
|
|
254
|
+
| `autoResize` | `Boolean` | `true` | Re-scale on container resize |
|
|
252
255
|
|
|
253
256
|
#### Emits
|
|
254
257
|
|
|
@@ -270,6 +273,28 @@ type NoteData = Omit<AnnotationNote, 'view' | 'editable'>;
|
|
|
270
273
|
| `getNotes()` | `NoteData[]` | Get current annotations |
|
|
271
274
|
| `notes` | `Ref<NoteData[]>` | Reactive current notes |
|
|
272
275
|
|
|
276
|
+
## Scaling
|
|
277
|
+
|
|
278
|
+
The plugin automatically detects the rendered image size and scales annotations accordingly. Annotation coordinates are always stored in natural (original) image pixels, so the same data works regardless of display size.
|
|
279
|
+
|
|
280
|
+
- **CSS constraints** (e.g., `max-width: 500px`) are respected automatically
|
|
281
|
+
- **HTML size attributes** (e.g., `width="400"`) work as expected
|
|
282
|
+
- **Responsive layouts** are supported via `ResizeObserver` — annotations reposition when the container resizes
|
|
283
|
+
- Set `autoResize: false` to disable dynamic resizing (annotations scale once at initialization)
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
// Works automatically — no configuration needed
|
|
287
|
+
annotate(document.getElementById('myImage'), {
|
|
288
|
+
notes: [/* coordinates in natural image pixels */],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Disable dynamic resizing
|
|
292
|
+
annotate(document.getElementById('myImage'), {
|
|
293
|
+
autoResize: false,
|
|
294
|
+
notes: [/* ... */],
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
273
298
|
## Tree Shaking
|
|
274
299
|
|
|
275
300
|
Each entry point (`annotate-image`, `annotate-image/jquery`, `annotate-image/react`, `annotate-image/vue`) is a separate bundle. Importing one does not pull in the others. Unused framework adapters are excluded automatically by bundlers that support package `exports`.
|