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/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
- 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";
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
- this.note.top = pos.top;
127
- this.note.left = pos.left;
128
- this.note.width = size.width;
129
- this.note.height = size.height;
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 = 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";
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 areaPos = readInlinePosition(editable.area);
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 = 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;
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
- const width = img.width;
534
- const height = img.height;
535
- if (width === 0 || height === 0) {
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.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";
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
- img.style.display = "none";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "annotate-image",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.2",
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": {
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`.