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/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
- this.area.style.height = this.note.height + "px";
34
- this.area.style.width = this.note.width + "px";
35
- this.area.style.left = this.note.left + "px";
36
- this.area.style.top = this.note.top + "px";
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: applyRect
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
- this.note.top = pos.top;
127
- this.note.left = pos.left;
128
- this.note.width = size.width;
129
- this.note.height = size.height;
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 = this.note.height + "px";
242
- innerDiv.style.width = this.note.width + "px";
243
- this.area.style.left = this.note.left + "px";
244
- this.area.style.top = this.note.top + "px";
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 areaPos = readInlinePosition(editable.area);
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 = areaSize.height + "px";
254
- innerDiv.style.width = areaSize.width + "px";
255
- this.area.style.left = areaPos.left + "px";
256
- this.area.style.top = areaPos.top + "px";
257
- this.note.top = areaPos.top;
258
- this.note.left = areaPos.left;
259
- this.note.height = areaSize.height;
260
- this.note.width = areaSize.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 = startLeft, top = startTop, width = startWidth, height = startHeight;
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
- const width = img.width;
534
- const height = img.height;
535
- if (width === 0 || height === 0) {
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.nextSibling);
555
- this.canvas.style.height = height + "px";
556
- this.canvas.style.width = width + "px";
557
- this.canvas.style.backgroundImage = 'url("' + img.src + '")';
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
- img.style.display = "none";
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.1",
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": "live-server --port=8080 --open=demo/index.html --watch=dist,demo",
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": "live-server --port=8080 --no-browser --watch=dist,demo"
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.2",
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.33",
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.27.3",
82
- "eslint": "^9.39.2",
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.0.0",
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.55.0",
94
- "vitest": "^4.0.18",
95
- "vue": "^3.5.28"
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
- Run the demo server locally:
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 multiple instances.
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