annotate-image 2.0.0-beta.1 → 2.0.0-beta.3
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 +173 -41
- package/dist/core.min.js +1 -1
- package/dist/css/annotate.min.css +1 -1
- package/dist/jquery.js +172 -41
- package/dist/jquery.min.js +1 -1
- package/dist/react.js +174 -42
- package/dist/types/annotate-edit.d.ts +2 -0
- package/dist/types/annotate-image.d.ts +42 -0
- package/dist/types/annotate-view.d.ts +1 -1
- package/dist/types/positioning.d.ts +31 -0
- package/dist/types/react.d.ts +2 -0
- package/dist/types/types.d.ts +4 -0
- package/dist/types/vue.d.ts +11 -0
- package/dist/vue.js +176 -42
- package/package.json +12 -12
- package/readme.md +111 -2
package/dist/vue.js
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
// src/vue.ts
|
|
2
2
|
import { defineComponent, ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, h } from "vue";
|
|
3
3
|
|
|
4
|
+
// src/positioning.ts
|
|
5
|
+
function clampNote(note, naturalWidth, naturalHeight) {
|
|
6
|
+
const width = Math.min(note.width, naturalWidth);
|
|
7
|
+
const height = Math.min(note.height, naturalHeight);
|
|
8
|
+
const left = Math.max(0, Math.min(note.left, naturalWidth - width));
|
|
9
|
+
const top = Math.max(0, Math.min(note.top, naturalHeight - height));
|
|
10
|
+
return { top, left, width, height };
|
|
11
|
+
}
|
|
12
|
+
function clampNotes(notes, naturalWidth, naturalHeight) {
|
|
13
|
+
for (const note of notes) {
|
|
14
|
+
Object.assign(note, clampNote(note, naturalWidth, naturalHeight));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function computeNoteLeft(noteWidth, areaLeftInViewport, areaWidth, viewportWidth) {
|
|
18
|
+
let left = (areaWidth - noteWidth) / 2;
|
|
19
|
+
const noteLeftInViewport = areaLeftInViewport + left;
|
|
20
|
+
const noteRightInViewport = noteLeftInViewport + noteWidth;
|
|
21
|
+
if (noteRightInViewport > viewportWidth) {
|
|
22
|
+
left -= noteRightInViewport - viewportWidth;
|
|
23
|
+
}
|
|
24
|
+
const adjustedNoteLeft = areaLeftInViewport + left;
|
|
25
|
+
if (adjustedNoteLeft < 0) {
|
|
26
|
+
left -= adjustedNoteLeft;
|
|
27
|
+
}
|
|
28
|
+
return left;
|
|
29
|
+
}
|
|
30
|
+
|
|
4
31
|
// src/annotate-edit.ts
|
|
5
32
|
var DEFAULT_NOTE_TOP = 30;
|
|
6
33
|
var DEFAULT_NOTE_LEFT = 30;
|
|
@@ -30,10 +57,11 @@ var AnnotateEdit = class {
|
|
|
30
57
|
};
|
|
31
58
|
}
|
|
32
59
|
this.area = image.editOverlay.querySelector(".image-annotate-edit-area");
|
|
33
|
-
|
|
34
|
-
this.area.style.
|
|
35
|
-
this.area.style.
|
|
36
|
-
this.area.style.
|
|
60
|
+
const rendered = image.toRendered(this.note);
|
|
61
|
+
this.area.style.height = rendered.height + "px";
|
|
62
|
+
this.area.style.width = rendered.width + "px";
|
|
63
|
+
this.area.style.left = rendered.left + "px";
|
|
64
|
+
this.area.style.top = rendered.top + "px";
|
|
37
65
|
this.form = document.createElement("div");
|
|
38
66
|
this.form.className = "image-annotate-edit-form";
|
|
39
67
|
const formEl = document.createElement("form");
|
|
@@ -49,6 +77,8 @@ var AnnotateEdit = class {
|
|
|
49
77
|
formEl.appendChild(this.textarea);
|
|
50
78
|
this.form.appendChild(formEl);
|
|
51
79
|
this.area.appendChild(this.form);
|
|
80
|
+
this.positionForm();
|
|
81
|
+
this.textarea.focus();
|
|
52
82
|
this.form.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
53
83
|
const area = this.area;
|
|
54
84
|
const applyRect = (rect) => {
|
|
@@ -60,7 +90,10 @@ var AnnotateEdit = class {
|
|
|
60
90
|
this.handlers.makeResizable(area, {
|
|
61
91
|
containment: image.canvas,
|
|
62
92
|
onResize: applyRect,
|
|
63
|
-
onStop:
|
|
93
|
+
onStop: (rect) => {
|
|
94
|
+
applyRect(rect);
|
|
95
|
+
this.positionForm();
|
|
96
|
+
}
|
|
64
97
|
});
|
|
65
98
|
this.handlers.makeDraggable(area, {
|
|
66
99
|
containment: image.canvas,
|
|
@@ -71,9 +104,9 @@ var AnnotateEdit = class {
|
|
|
71
104
|
onStop: (pos) => {
|
|
72
105
|
area.style.left = pos.left + "px";
|
|
73
106
|
area.style.top = pos.top + "px";
|
|
107
|
+
this.positionForm();
|
|
74
108
|
}
|
|
75
109
|
});
|
|
76
|
-
this.textarea.focus();
|
|
77
110
|
this.form.addEventListener("keydown", (e) => {
|
|
78
111
|
if (e.key === "Escape") {
|
|
79
112
|
const cancelBtn = this.form.querySelector(".image-annotate-edit-close");
|
|
@@ -89,6 +122,13 @@ var AnnotateEdit = class {
|
|
|
89
122
|
}
|
|
90
123
|
this.addCancelButton(buttonRow);
|
|
91
124
|
}
|
|
125
|
+
/** Recompute the form's horizontal position relative to the area. */
|
|
126
|
+
positionForm() {
|
|
127
|
+
const formRect = this.form.getBoundingClientRect();
|
|
128
|
+
const areaRect = this.area.getBoundingClientRect();
|
|
129
|
+
const left = computeNoteLeft(formRect.width, areaRect.left, areaRect.width, window.innerWidth);
|
|
130
|
+
this.form.style.left = left + "px";
|
|
131
|
+
}
|
|
92
132
|
/** Tear down the edit form and interaction handlers. */
|
|
93
133
|
destroy() {
|
|
94
134
|
this.image.activeEdit = null;
|
|
@@ -120,13 +160,20 @@ var AnnotateEdit = class {
|
|
|
120
160
|
}
|
|
121
161
|
this.image.notifySave(stripInternals(this.note));
|
|
122
162
|
this.destroy();
|
|
163
|
+
this.image.flushPendingRescale();
|
|
123
164
|
};
|
|
124
165
|
const pos = readInlinePosition(this.area);
|
|
125
166
|
const size = readInlineSize(this.area);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
167
|
+
const natural = this.image.toNatural({
|
|
168
|
+
top: pos.top,
|
|
169
|
+
left: pos.left,
|
|
170
|
+
width: size.width,
|
|
171
|
+
height: size.height
|
|
172
|
+
});
|
|
173
|
+
this.note.top = natural.top;
|
|
174
|
+
this.note.left = natural.left;
|
|
175
|
+
this.note.width = natural.width;
|
|
176
|
+
this.note.height = natural.height;
|
|
130
177
|
this.note.text = text;
|
|
131
178
|
if (this.image.api.save) {
|
|
132
179
|
this.busy = true;
|
|
@@ -160,6 +207,7 @@ var AnnotateEdit = class {
|
|
|
160
207
|
const idx = this.image.notes.indexOf(this.note);
|
|
161
208
|
if (idx !== -1) this.image.notes.splice(idx, 1);
|
|
162
209
|
this.image.notifyDelete(stripInternals(this.note));
|
|
210
|
+
this.image.flushPendingRescale();
|
|
163
211
|
};
|
|
164
212
|
if (this.image.api.delete) {
|
|
165
213
|
this.busy = true;
|
|
@@ -235,36 +283,42 @@ var AnnotateView = class {
|
|
|
235
283
|
});
|
|
236
284
|
}
|
|
237
285
|
}
|
|
238
|
-
/** Apply the note's position and dimensions to the area element. */
|
|
286
|
+
/** Apply the note's position and dimensions to the area element, scaled to rendered size. */
|
|
239
287
|
setPosition() {
|
|
288
|
+
const rendered = this.image.toRendered(this.note);
|
|
240
289
|
const innerDiv = this.area.firstElementChild;
|
|
241
|
-
innerDiv.style.height =
|
|
242
|
-
innerDiv.style.width =
|
|
243
|
-
this.area.style.left =
|
|
244
|
-
this.area.style.top =
|
|
290
|
+
innerDiv.style.height = rendered.height + "px";
|
|
291
|
+
innerDiv.style.width = rendered.width + "px";
|
|
292
|
+
this.area.style.left = rendered.left + "px";
|
|
293
|
+
this.area.style.top = rendered.top + "px";
|
|
245
294
|
}
|
|
246
295
|
/** Update the view's position, size, and text from the edit area after a save. */
|
|
247
296
|
resetPosition(editable, text) {
|
|
248
297
|
this.tooltip.textContent = text;
|
|
249
298
|
this.tooltip.style.display = "none";
|
|
250
|
-
const
|
|
251
|
-
const areaSize = readInlineSize(editable.area);
|
|
299
|
+
const rendered = this.image.toRendered(editable.note);
|
|
252
300
|
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 =
|
|
301
|
+
innerDiv.style.height = rendered.height + "px";
|
|
302
|
+
innerDiv.style.width = rendered.width + "px";
|
|
303
|
+
this.area.style.left = rendered.left + "px";
|
|
304
|
+
this.area.style.top = rendered.top + "px";
|
|
305
|
+
this.note.top = editable.note.top;
|
|
306
|
+
this.note.left = editable.note.left;
|
|
307
|
+
this.note.height = editable.note.height;
|
|
308
|
+
this.note.width = editable.note.width;
|
|
261
309
|
this.note.text = text;
|
|
262
310
|
this.note.id = editable.note.id;
|
|
263
311
|
this.editable = true;
|
|
264
312
|
}
|
|
265
313
|
/** Show the tooltip and apply hover styling. */
|
|
266
314
|
show() {
|
|
315
|
+
this.tooltip.style.visibility = "hidden";
|
|
267
316
|
this.tooltip.style.display = "block";
|
|
317
|
+
const noteRect = this.tooltip.getBoundingClientRect();
|
|
318
|
+
const areaRect = this.area.getBoundingClientRect();
|
|
319
|
+
const left = computeNoteLeft(noteRect.width, areaRect.left, areaRect.width, window.innerWidth);
|
|
320
|
+
this.tooltip.style.left = left + "px";
|
|
321
|
+
this.tooltip.style.visibility = "";
|
|
268
322
|
if (!this.editable) {
|
|
269
323
|
this.area.classList.add("image-annotate-area-hover");
|
|
270
324
|
} else {
|
|
@@ -362,17 +416,19 @@ function destroyDraggable(el) {
|
|
|
362
416
|
var MIN_SIZE = 10;
|
|
363
417
|
var CORNERS = ["nw", "ne", "sw", "se"];
|
|
364
418
|
function computeResize(corner, startLeft, startTop, startWidth, startHeight, dx, dy) {
|
|
365
|
-
let left
|
|
419
|
+
let left, top, width, height;
|
|
366
420
|
if (corner === "nw" || corner === "sw") {
|
|
367
421
|
left = startLeft + dx;
|
|
368
422
|
width = startWidth - dx;
|
|
369
423
|
} else {
|
|
424
|
+
left = startLeft;
|
|
370
425
|
width = startWidth + dx;
|
|
371
426
|
}
|
|
372
427
|
if (corner === "nw" || corner === "ne") {
|
|
373
428
|
top = startTop + dy;
|
|
374
429
|
height = startHeight - dy;
|
|
375
430
|
} else {
|
|
431
|
+
top = startTop;
|
|
376
432
|
height = startHeight + dy;
|
|
377
433
|
}
|
|
378
434
|
if (width < MIN_SIZE) {
|
|
@@ -527,17 +583,30 @@ var AnnotateImage = class {
|
|
|
527
583
|
this._mode = "view";
|
|
528
584
|
this.activeEdit = null;
|
|
529
585
|
this.destroyed = false;
|
|
586
|
+
this.pendingRescale = false;
|
|
587
|
+
this.originalParent = null;
|
|
588
|
+
this.originalNextSibling = null;
|
|
530
589
|
this.options = options;
|
|
531
590
|
this.handlers = createDefaultHandlers();
|
|
532
591
|
this.img = img;
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
592
|
+
this.naturalWidth = img.naturalWidth || img.width;
|
|
593
|
+
this.naturalHeight = img.naturalHeight || img.height;
|
|
594
|
+
const rendered = img.getBoundingClientRect();
|
|
595
|
+
const renderedWidth = rendered.width || img.width;
|
|
596
|
+
const renderedHeight = rendered.height || img.height;
|
|
597
|
+
if (this.naturalWidth === 0 || this.naturalHeight === 0) {
|
|
536
598
|
throw new Error("image-annotate: image must have non-zero dimensions (is the image loaded?)");
|
|
537
599
|
}
|
|
600
|
+
this.scaleX = renderedWidth / this.naturalWidth;
|
|
601
|
+
this.scaleY = renderedHeight / this.naturalHeight;
|
|
538
602
|
this.notes = options.notes.map((n) => ({ ...n }));
|
|
603
|
+
this.originalParent = img.parentNode;
|
|
604
|
+
this.originalNextSibling = img.nextSibling;
|
|
539
605
|
this.canvas = document.createElement("div");
|
|
540
606
|
this.canvas.className = "image-annotate-canvas";
|
|
607
|
+
if (options.theme) {
|
|
608
|
+
this.canvas.dataset.theme = options.theme;
|
|
609
|
+
}
|
|
541
610
|
this.viewOverlay = document.createElement("div");
|
|
542
611
|
this.viewOverlay.className = "image-annotate-view";
|
|
543
612
|
this.editOverlay = document.createElement("div");
|
|
@@ -546,19 +615,13 @@ var AnnotateImage = class {
|
|
|
546
615
|
const editArea = document.createElement("div");
|
|
547
616
|
editArea.className = "image-annotate-edit-area";
|
|
548
617
|
this.editOverlay.appendChild(editArea);
|
|
549
|
-
this.canvas.appendChild(this.viewOverlay);
|
|
550
|
-
this.canvas.appendChild(this.editOverlay);
|
|
551
618
|
if (!img.parentNode) {
|
|
552
619
|
throw new Error("image-annotate: image must be in the DOM before initialization");
|
|
553
620
|
}
|
|
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";
|
|
621
|
+
img.parentNode.insertBefore(this.canvas, img);
|
|
622
|
+
this.canvas.appendChild(img);
|
|
623
|
+
this.canvas.appendChild(this.viewOverlay);
|
|
624
|
+
this.canvas.appendChild(this.editOverlay);
|
|
562
625
|
this.api = this.options.api ? normalizeApi(this.options.api) : {};
|
|
563
626
|
if (this.api.load) {
|
|
564
627
|
this.loadFromApi();
|
|
@@ -568,7 +631,38 @@ var AnnotateImage = class {
|
|
|
568
631
|
if (this.options.editable) {
|
|
569
632
|
this.createButton();
|
|
570
633
|
}
|
|
571
|
-
|
|
634
|
+
if (options.autoResize !== false && typeof ResizeObserver !== "undefined") {
|
|
635
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
636
|
+
const entry = entries[0];
|
|
637
|
+
if (!entry) return;
|
|
638
|
+
const { width, height } = entry.contentRect;
|
|
639
|
+
if (width === 0 || height === 0) return;
|
|
640
|
+
this.rescale(width, height);
|
|
641
|
+
});
|
|
642
|
+
this.resizeObserver.observe(this.canvas);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/** Convert a rect from natural image coordinates to rendered (scaled) coordinates. */
|
|
646
|
+
toRendered(rect) {
|
|
647
|
+
return {
|
|
648
|
+
top: rect.top * this.scaleY,
|
|
649
|
+
left: rect.left * this.scaleX,
|
|
650
|
+
width: rect.width * this.scaleX,
|
|
651
|
+
height: rect.height * this.scaleY
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
/** Convert a rect from rendered (scaled) coordinates to natural image coordinates. */
|
|
655
|
+
toNatural(rect) {
|
|
656
|
+
const result = {
|
|
657
|
+
top: rect.top / this.scaleY,
|
|
658
|
+
left: rect.left / this.scaleX,
|
|
659
|
+
width: rect.width / this.scaleX,
|
|
660
|
+
height: rect.height / this.scaleY
|
|
661
|
+
};
|
|
662
|
+
if (!isFinite(result.top) || !isFinite(result.left) || !isFinite(result.width) || !isFinite(result.height)) {
|
|
663
|
+
throw new Error("image-annotate: scale conversion produced non-finite coordinates");
|
|
664
|
+
}
|
|
665
|
+
return result;
|
|
572
666
|
}
|
|
573
667
|
/** Current interaction mode — 'view' for browsing, 'edit' when an annotation is being created or modified. */
|
|
574
668
|
get mode() {
|
|
@@ -622,6 +716,7 @@ var AnnotateImage = class {
|
|
|
622
716
|
/** Rebuild annotation views from the current notes array. */
|
|
623
717
|
load() {
|
|
624
718
|
this.destroyViews();
|
|
719
|
+
clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
|
|
625
720
|
this.createViews();
|
|
626
721
|
this.notifyLoad();
|
|
627
722
|
}
|
|
@@ -640,8 +735,15 @@ var AnnotateImage = class {
|
|
|
640
735
|
if (this.button) {
|
|
641
736
|
this.button.remove();
|
|
642
737
|
}
|
|
738
|
+
if (this.resizeObserver) {
|
|
739
|
+
this.resizeObserver.disconnect();
|
|
740
|
+
this.resizeObserver = void 0;
|
|
741
|
+
}
|
|
742
|
+
if (this.originalParent && this.originalParent.isConnected) {
|
|
743
|
+
const ref2 = this.originalNextSibling?.parentNode === this.originalParent ? this.originalNextSibling : null;
|
|
744
|
+
this.originalParent.insertBefore(this.img, ref2);
|
|
745
|
+
}
|
|
643
746
|
this.canvas.remove();
|
|
644
|
-
this.img.style.display = "";
|
|
645
747
|
}
|
|
646
748
|
/** Cancel the active edit (if any) and return to view mode. */
|
|
647
749
|
cancelEdit() {
|
|
@@ -649,6 +751,34 @@ var AnnotateImage = class {
|
|
|
649
751
|
this.activeEdit.destroy();
|
|
650
752
|
this.setMode("view");
|
|
651
753
|
}
|
|
754
|
+
this.flushPendingRescale();
|
|
755
|
+
}
|
|
756
|
+
/** Recompute scale factors, deferring if an edit is active. */
|
|
757
|
+
rescale(renderedWidth, renderedHeight) {
|
|
758
|
+
if (this.mode === "edit") {
|
|
759
|
+
this.pendingRescale = true;
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
this.applyRescale(renderedWidth, renderedHeight);
|
|
763
|
+
}
|
|
764
|
+
/** Apply new scale factors and re-render all views. */
|
|
765
|
+
applyRescale(renderedWidth, renderedHeight) {
|
|
766
|
+
const newScaleX = renderedWidth / this.naturalWidth;
|
|
767
|
+
const newScaleY = renderedHeight / this.naturalHeight;
|
|
768
|
+
if (newScaleX === this.scaleX && newScaleY === this.scaleY) return;
|
|
769
|
+
this.scaleX = newScaleX;
|
|
770
|
+
this.scaleY = newScaleY;
|
|
771
|
+
this.destroyViews();
|
|
772
|
+
this.createViews();
|
|
773
|
+
}
|
|
774
|
+
/** @internal Flush any deferred rescale after an edit completes. */
|
|
775
|
+
flushPendingRescale() {
|
|
776
|
+
if (!this.pendingRescale) return;
|
|
777
|
+
this.pendingRescale = false;
|
|
778
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
779
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
780
|
+
this.applyRescale(rect.width, rect.height);
|
|
781
|
+
}
|
|
652
782
|
}
|
|
653
783
|
/** Replace all annotations with new data. Does not fire lifecycle callbacks. */
|
|
654
784
|
setNotes(notes) {
|
|
@@ -695,6 +825,7 @@ var AnnotateImage = class {
|
|
|
695
825
|
this.api.load().then((notes) => {
|
|
696
826
|
this.destroyViews();
|
|
697
827
|
this.notes = notes;
|
|
828
|
+
clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
|
|
698
829
|
this.createViews();
|
|
699
830
|
this.notifyLoad();
|
|
700
831
|
}).catch((err) => {
|
|
@@ -731,7 +862,9 @@ var AnnotateImage2 = defineComponent({
|
|
|
731
862
|
/** Annotations to render. */
|
|
732
863
|
notes: { type: Array },
|
|
733
864
|
/** Enable annotation editing. Default: true. */
|
|
734
|
-
editable: { type: Boolean, default: true }
|
|
865
|
+
editable: { type: Boolean, default: true },
|
|
866
|
+
/** Enable automatic re-scaling when the container resizes. Default: true. */
|
|
867
|
+
autoResize: { type: Boolean, default: true }
|
|
735
868
|
},
|
|
736
869
|
emits: {
|
|
737
870
|
save: (_note) => true,
|
|
@@ -749,6 +882,7 @@ var AnnotateImage2 = defineComponent({
|
|
|
749
882
|
try {
|
|
750
883
|
const instance = new AnnotateImage(imgRef.value, {
|
|
751
884
|
editable: props.editable,
|
|
885
|
+
autoResize: props.autoResize,
|
|
752
886
|
notes: props.notes ? props.notes.slice() : [],
|
|
753
887
|
onChange: (notes) => {
|
|
754
888
|
currentNotes.value = notes;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "annotate-image",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.3",
|
|
4
4
|
"description": "Create Flickr-like comment annotations on images — draw rectangles, add notes, save via AJAX or static data",
|
|
5
5
|
"license": "GPL-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -64,35 +64,35 @@
|
|
|
64
64
|
"demo:watch:react": "esbuild src/react.tsx --bundle --format=esm --external:react --external:react-dom --outfile=dist/react.js --watch",
|
|
65
65
|
"demo:watch:vue": "esbuild src/vue.ts --bundle --format=esm --external:vue --outfile=dist/vue.js --watch",
|
|
66
66
|
"demo:watch:css": "esbuild src/annotation.css --minify --outfile=dist/css/annotate.min.css --watch",
|
|
67
|
-
"demo:serve": "
|
|
67
|
+
"demo:serve": "browser-sync start --server --port 8080 --startPath demo/index.html --files 'dist/**/*' 'demo/**/*'",
|
|
68
68
|
"demo:ci": "npm run build && concurrently -k \"npm:demo:watch:*\" \"npm:demo:serve:ci\"",
|
|
69
|
-
"demo:serve:ci": "
|
|
69
|
+
"demo:serve:ci": "browser-sync start --server --port 8080 --no-open --files 'dist/**/*' 'demo/**/*'"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@eslint/js": "^9.39.
|
|
72
|
+
"@eslint/js": "^9.39.4",
|
|
73
73
|
"@playwright/test": "^1.58.2",
|
|
74
74
|
"@testing-library/dom": "^10.4.1",
|
|
75
75
|
"@testing-library/react": "^16.3.2",
|
|
76
|
-
"@types/jquery": "^3.5.
|
|
76
|
+
"@types/jquery": "^3.5.34",
|
|
77
77
|
"@types/react": "^18.3.28",
|
|
78
78
|
"@types/react-dom": "^18.3.7",
|
|
79
79
|
"@vue/test-utils": "^2.4.6",
|
|
80
|
+
"browser-sync": "^3.0.4",
|
|
80
81
|
"concurrently": "^9.2.1",
|
|
81
|
-
"esbuild": "^0.
|
|
82
|
-
"eslint": "^9.39.
|
|
82
|
+
"esbuild": "^0.28.1",
|
|
83
|
+
"eslint": "^9.39.4",
|
|
83
84
|
"eslint-config-prettier": "^10.1.8",
|
|
84
85
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
85
86
|
"eslint-plugin-vue": "^10.8.0",
|
|
86
87
|
"jquery": "^3.7.1",
|
|
87
|
-
"jsdom": "^28.
|
|
88
|
-
"live-server": "^1.2.2",
|
|
88
|
+
"jsdom": "^28.1.0",
|
|
89
89
|
"prettier": "^3.8.1",
|
|
90
90
|
"react": "^18.3.1",
|
|
91
91
|
"react-dom": "^18.3.1",
|
|
92
92
|
"typescript": "^5.9.3",
|
|
93
|
-
"typescript-eslint": "^8.
|
|
94
|
-
"vitest": "^4.0
|
|
95
|
-
"vue": "^3.5.
|
|
93
|
+
"typescript-eslint": "^8.57.0",
|
|
94
|
+
"vitest": "^4.1.0",
|
|
95
|
+
"vue": "^3.5.30"
|
|
96
96
|
},
|
|
97
97
|
"peerDependencies": {
|
|
98
98
|
"jquery": ">=3.7.0",
|
package/readme.md
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
# Annotate Image
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://pullpatchpush.com/annotate-image"><img src="assets/image-annotate-preview.jpg" alt="Annotate Image preview" width="387"></a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/annotate-image"><img src="https://img.shields.io/npm/v/annotate-image" alt="npm version"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/annotate-image"><img src="https://img.shields.io/npm/dm/annotate-image" alt="npm downloads"></a>
|
|
10
|
+
<a href="https://bundlephobia.com/package/annotate-image"><img src="https://img.shields.io/bundlephobia/minzip/annotate-image" alt="bundle size"></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/TypeScript-strict-blue" alt="TypeScript strict">
|
|
12
|
+
<a href="https://www.npmjs.com/package/annotate-image"><img src="https://img.shields.io/npm/l/annotate-image" alt="license"></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
3
15
|
A JavaScript image annotation plugin that creates Flickr-like comment annotations on images. Users can draw rectangular regions on images, add text notes, and persist annotations via callbacks or AJAX.
|
|
4
16
|
|
|
5
17
|
Works standalone (vanilla JS), or with jQuery, React, or Vue. Framework adapters are tree-shakeable — only the one you import gets bundled.
|
|
6
18
|
|
|
19
|
+
**[Documentation & Live Demo](https://pullpatchpush.com/annotate-image)**
|
|
20
|
+
|
|
7
21
|
## Installation
|
|
8
22
|
|
|
9
23
|
```sh
|
|
@@ -171,6 +185,8 @@ Creates an annotation layer on an image element. Returns an `AnnotateImage` inst
|
|
|
171
185
|
| `onDelete` | `(note: NoteData) => void` | — | Called after a note is deleted |
|
|
172
186
|
| `onLoad` | `(notes: NoteData[]) => void` | — | Called after notes are loaded |
|
|
173
187
|
| `onError` | `(ctx: AnnotateErrorContext) => void` | — | Called on API errors (defaults to `console.error`) |
|
|
188
|
+
| `autoResize` | `boolean` | `true` | Re-scale annotations when the container resizes |
|
|
189
|
+
| `theme` | `string` | — | CSS theme name; sets `data-theme` on the canvas for variable scoping |
|
|
174
190
|
|
|
175
191
|
#### `AnnotateApi`
|
|
176
192
|
|
|
@@ -228,6 +244,7 @@ type NoteData = Omit<AnnotationNote, 'view' | 'editable'>;
|
|
|
228
244
|
| `onDelete` | `(note: NoteData) => void` | — | Note deleted |
|
|
229
245
|
| `onLoad` | `(notes: NoteData[]) => void` | — | Notes loaded |
|
|
230
246
|
| `onError` | `(ctx: AnnotateErrorContext) => void` | — | Error occurred |
|
|
247
|
+
| `autoResize` | `boolean` | `true` | Re-scale on container resize |
|
|
231
248
|
|
|
232
249
|
#### Ref Methods (`AnnotateImageRef`)
|
|
233
250
|
|
|
@@ -249,6 +266,7 @@ type NoteData = Omit<AnnotationNote, 'view' | 'editable'>;
|
|
|
249
266
|
| `height` | `Number` | — | Image height in pixels |
|
|
250
267
|
| `notes` | `AnnotationNote[]` | — | Initial annotations |
|
|
251
268
|
| `editable` | `Boolean` | `true` | Enable editing |
|
|
269
|
+
| `autoResize` | `Boolean` | `true` | Re-scale on container resize |
|
|
252
270
|
|
|
253
271
|
#### Emits
|
|
254
272
|
|
|
@@ -270,6 +288,97 @@ type NoteData = Omit<AnnotationNote, 'view' | 'editable'>;
|
|
|
270
288
|
| `getNotes()` | `NoteData[]` | Get current annotations |
|
|
271
289
|
| `notes` | `Ref<NoteData[]>` | Reactive current notes |
|
|
272
290
|
|
|
291
|
+
## Scaling
|
|
292
|
+
|
|
293
|
+
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.
|
|
294
|
+
|
|
295
|
+
- **CSS constraints** (e.g., `max-width: 500px`) are respected automatically
|
|
296
|
+
- **HTML size attributes** (e.g., `width="400"`) work as expected
|
|
297
|
+
- **Responsive layouts** are supported via `ResizeObserver` — annotations reposition when the container resizes
|
|
298
|
+
- Set `autoResize: false` to disable dynamic resizing (annotations scale once at initialization)
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
// Works automatically — no configuration needed
|
|
302
|
+
annotate(document.getElementById('myImage'), {
|
|
303
|
+
notes: [/* coordinates in natural image pixels */],
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Disable dynamic resizing
|
|
307
|
+
annotate(document.getElementById('myImage'), {
|
|
308
|
+
autoResize: false,
|
|
309
|
+
notes: [/* ... */],
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Theming
|
|
314
|
+
|
|
315
|
+
The plugin uses CSS custom properties for all visual styling. Override these variables in your CSS to create custom themes.
|
|
316
|
+
|
|
317
|
+
### Using the `theme` option
|
|
318
|
+
|
|
319
|
+
Set `theme` in options to add a `data-theme` attribute to the canvas element, enabling CSS scoping:
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
annotate(document.getElementById('myImage'), {
|
|
323
|
+
theme: 'dark',
|
|
324
|
+
notes: [/* ... */],
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
```css
|
|
329
|
+
.image-annotate-canvas[data-theme="dark"] {
|
|
330
|
+
--image-annotate-area-border: rgba(255, 255, 255, 0.7);
|
|
331
|
+
--image-annotate-area-bg: rgba(100, 149, 237, 0.15);
|
|
332
|
+
--image-annotate-note-bg: #2a2a3e;
|
|
333
|
+
--image-annotate-note-text: #e0e0f0;
|
|
334
|
+
--image-annotate-note-radius: 4px;
|
|
335
|
+
--image-annotate-note-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
|
336
|
+
/* ... see demo for full example */
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Multiple instances on the same page can use different themes.
|
|
341
|
+
|
|
342
|
+
Button icons (save, delete, cancel) use CSS `mask-image`, so their color automatically follows `--image-annotate-button-text`. Dark themes just need to set the text color — no icon overrides required.
|
|
343
|
+
|
|
344
|
+
### Available CSS variables
|
|
345
|
+
|
|
346
|
+
Set on `.image-annotate-canvas`:
|
|
347
|
+
|
|
348
|
+
| Variable | Default | Description |
|
|
349
|
+
|---|---|---|
|
|
350
|
+
| `--image-annotate-canvas-border` | `none` | Border around the canvas |
|
|
351
|
+
| `--image-annotate-font-family` | `Verdana, sans-serif` | Font for notes and buttons |
|
|
352
|
+
| `--image-annotate-font-size` | `12px` | Font size for notes and buttons |
|
|
353
|
+
| `--image-annotate-area-border` | `#000` | Annotation rectangle border color |
|
|
354
|
+
| `--image-annotate-area-inner-border` | `#fff` | Annotation inner border color |
|
|
355
|
+
| `--image-annotate-area-bg` | `transparent` | Annotation rectangle fill |
|
|
356
|
+
| `--image-annotate-area-border-width` | `1px` | Annotation border thickness |
|
|
357
|
+
| `--image-annotate-area-border-style` | `solid` | Annotation border style |
|
|
358
|
+
| `--image-annotate-area-radius` | `0` | Annotation border-radius |
|
|
359
|
+
| `--image-annotate-hover-color` | `yellow` | Hover border color (read-only) |
|
|
360
|
+
| `--image-annotate-hover-editable-color` | `#00ad00` | Hover border color (editable) |
|
|
361
|
+
| `--image-annotate-hover-bg` | `transparent` | Hover background fill |
|
|
362
|
+
| `--image-annotate-note-bg` | `#e7ffe7` | Tooltip background |
|
|
363
|
+
| `--image-annotate-note-border` | `#397f39` | Tooltip border |
|
|
364
|
+
| `--image-annotate-note-text` | `#000` | Tooltip text color |
|
|
365
|
+
| `--image-annotate-note-radius` | `0` | Tooltip border-radius |
|
|
366
|
+
| `--image-annotate-note-shadow` | `none` | Tooltip box-shadow |
|
|
367
|
+
| `--image-annotate-note-max-width` | `300px` | Tooltip max width |
|
|
368
|
+
| `--image-annotate-edit-bg` | `#fffee3` | Edit form background |
|
|
369
|
+
| `--image-annotate-edit-border` | `#000` | Edit form border |
|
|
370
|
+
| `--image-annotate-edit-radius` | `0` | Edit form border-radius |
|
|
371
|
+
| `--image-annotate-edit-shadow` | `none` | Edit form box-shadow |
|
|
372
|
+
| `--image-annotate-edit-max-width` | `300px` | Edit form max width |
|
|
373
|
+
| `--image-annotate-button-bg` | `#fff` | Button background |
|
|
374
|
+
| `--image-annotate-button-bg-hover` | `#eee` | Button hover background |
|
|
375
|
+
| `--image-annotate-button-border` | `#ccc` | Button border |
|
|
376
|
+
| `--image-annotate-button-text` | `#000` | Button text and icon color |
|
|
377
|
+
| `--image-annotate-add-bg` | `rgba(0,0,0,0.4)` | Add Note button background |
|
|
378
|
+
| `--image-annotate-add-bg-hover` | `rgba(0,0,0,0.6)` | Add Note button hover background |
|
|
379
|
+
| `--image-annotate-add-border` | `1px solid rgba(255,255,255,0.5)` | Add Note button border |
|
|
380
|
+
| `--image-annotate-add-radius` | `4px` | Add Note button border-radius |
|
|
381
|
+
|
|
273
382
|
## Tree Shaking
|
|
274
383
|
|
|
275
384
|
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`.
|
|
@@ -301,13 +410,13 @@ The plugin supports keyboard navigation:
|
|
|
301
410
|
|
|
302
411
|
## Demos
|
|
303
412
|
|
|
304
|
-
|
|
413
|
+
Try the [live examples](https://pullpatchpush.com/annotate-image/examples) online, or run the demo server locally:
|
|
305
414
|
|
|
306
415
|
```sh
|
|
307
416
|
npm run demo
|
|
308
417
|
```
|
|
309
418
|
|
|
310
|
-
This opens a browser at `http://localhost:8080/demo/index.html` with links to demos including static annotations, AJAX loading, vanilla JS, React, Vue, and
|
|
419
|
+
This opens a browser at `http://localhost:8080/demo/index.html` with links to demos including static annotations, AJAX loading, vanilla JS, React, Vue, multiple instances, and CSS theming.
|
|
311
420
|
|
|
312
421
|
## Build
|
|
313
422
|
|