Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions demo/scaling.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
font-style: italic;
margin-top: 8px;
}
.img-padded-bordered {
padding: 12px;
border: 4px solid #666;
}
</style>
</head>
<body>
Expand Down Expand Up @@ -63,6 +67,12 @@ <h2>Responsive Container (50% width)</h2>
</div>
<p class="resize-hint">Try resizing the browser window.</p>
</div>

<div class="demo-section">
<h2>Image with Padding &amp; Border</h2>
<p>The image has 12px padding and a 4px border. Annotations should be positioned within the image content area, not the padding/border zone.</p>
<img id="padded-bordered" class="img-padded-bordered" src="images/starry-night.jpg" alt="Starry Night" width="960" height="760" style="max-width: 100%; height: auto;">
</div>
</div>

<script type="module">
Expand Down Expand Up @@ -90,6 +100,11 @@ <h2>Responsive Container (50% width)</h2>
editable: true,
notes: JSON.parse(JSON.stringify(notes))
});

annotate(document.getElementById('padded-bordered'), {
editable: true,
notes: JSON.parse(JSON.stringify(notes))
});
});
</script>
</body>
Expand Down
57 changes: 57 additions & 0 deletions e2e/scaling.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,61 @@ test.describe('Scaling', () => {
expect(newBox.width).toBeLessThan(initialBox.width);
}
});

test('padded/bordered image: renders 4 annotations', async ({ page }) => {
const canvas = page.locator('.image-annotate-canvas').nth(3);
const areas = canvas.locator('.image-annotate-area');
await expect(areas).toHaveCount(4);
});

test('padded/bordered image: annotations are within image content area', async ({ page }) => {
const canvas = page.locator('.image-annotate-canvas').nth(3);
const img = canvas.locator('img');
const imgBox = await img.boundingBox();
const areas = canvas.locator('.image-annotate-area');
const count = await areas.count();

// The image has 12px padding + 4px border = 16px inset on each side.
// Annotations should be within the content area (inset from border-box by 16px).
const inset = 12 + 4; // padding + border
for (let i = 0; i < count; i++) {
const areaBox = await areas.nth(i).boundingBox();
if (areaBox && imgBox) {
expect(areaBox.x).toBeGreaterThanOrEqual(imgBox.x + inset - 2);
expect(areaBox.y).toBeGreaterThanOrEqual(imgBox.y + inset - 2);
expect(areaBox.x + areaBox.width).toBeLessThanOrEqual(imgBox.x + imgBox.width - inset + 2);
expect(areaBox.y + areaBox.height).toBeLessThanOrEqual(imgBox.y + imgBox.height - inset + 2);
}
}
});

test('padded/bordered image: Add Note button is within image content area', async ({ page }) => {
const canvas = page.locator('.image-annotate-canvas').nth(3);
const img = canvas.locator('img');
const imgBox = await img.boundingBox();
const button = canvas.locator('.image-annotate-add');

// Hover to make button visible
await canvas.hover();
const btnBox = await button.boundingBox();

const inset = 12 + 4; // padding + border
if (btnBox && imgBox) {
// Button's right edge should be within the content area (with 8px margin from CSS)
expect(btnBox.x + btnBox.width).toBeLessThanOrEqual(imgBox.x + imgBox.width - inset + 1);
// Button's bottom edge should be within the content area
expect(btnBox.y + btnBox.height).toBeLessThanOrEqual(imgBox.y + imgBox.height - inset + 1);
}
});

test('padded/bordered image: hover shows tooltip', async ({ page }) => {
const canvas = page.locator('.image-annotate-canvas').nth(3);
const area = canvas.locator('.image-annotate-area').last();
const tooltip = area.locator('.image-annotate-note');

await expect(tooltip).toBeHidden();
await canvas.hover();
await area.hover();
await expect(tooltip).toBeVisible();
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "annotate-image",
"version": "2.0.0-beta.4",
"version": "2.0.0-beta.5",
"description": "Create Flickr-like comment annotations on images — draw rectangles, add notes, save via AJAX or static data",
"license": "GPL-2.0",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions src/annotate-edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,15 @@ export class AnnotateEdit {
area.style.height = rect.height + 'px';
};
this.handlers.makeResizable(area, {
containment: image.canvas,
containment: image.editOverlay,
onResize: applyRect,
onStop: (rect) => {
applyRect(rect);
this.positionForm();
},
});
this.handlers.makeDraggable(area, {
containment: image.canvas,
containment: image.editOverlay,
onDrag: (pos) => {
area.style.left = pos.left + 'px';
area.style.top = pos.top + 'px';
Expand Down
32 changes: 26 additions & 6 deletions src/annotate-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export class AnnotateImage {
scaleX: number;
/** Current vertical scale factor (rendered / natural). */
scaleY: number;
/** Padding+border insets from the image element (px). Zero when the image has no padding/border. */
readonly contentInset: { top: number; right: number; bottom: number; left: number };

/** Convert a rect from natural image coordinates to rendered (scaled) coordinates. */
toRendered(rect: { top: number; left: number; width: number; height: number }) {
Expand Down Expand Up @@ -125,12 +127,18 @@ export class AnnotateImage {
this.handlers = createDefaultHandlers();
this.img = img;

// Read natural and rendered dimensions
// Read natural and rendered dimensions, subtracting padding/border to get content area
this.naturalWidth = img.naturalWidth || img.width;
this.naturalHeight = img.naturalHeight || img.height;
const rendered = img.getBoundingClientRect();
const renderedWidth = rendered.width || img.width;
const renderedHeight = rendered.height || img.height;
const styles = getComputedStyle(img);
const insetTop = (parseFloat(styles.paddingTop) || 0) + (parseFloat(styles.borderTopWidth) || 0);
const insetRight = (parseFloat(styles.paddingRight) || 0) + (parseFloat(styles.borderRightWidth) || 0);
const insetBottom = (parseFloat(styles.paddingBottom) || 0) + (parseFloat(styles.borderBottomWidth) || 0);
const insetLeft = (parseFloat(styles.paddingLeft) || 0) + (parseFloat(styles.borderLeftWidth) || 0);
this.contentInset = { top: insetTop, right: insetRight, bottom: insetBottom, left: insetLeft };
const renderedWidth = (rendered.width || img.width) - insetLeft - insetRight;
const renderedHeight = (rendered.height || img.height) - insetTop - insetBottom;

if (this.naturalWidth === 0 || this.naturalHeight === 0) {
throw new Error('image-annotate: image must have non-zero dimensions (is the image loaded?)');
Expand All @@ -151,6 +159,14 @@ export class AnnotateImage {
this.canvas.dataset.theme = options.theme;
}

// Set content inset CSS vars for overlay/button positioning (only when non-zero)
if (insetTop || insetRight || insetBottom || insetLeft) {
this.canvas.style.setProperty('--image-annotate-content-top', `${insetTop}px`);
this.canvas.style.setProperty('--image-annotate-content-right', `${insetRight}px`);
this.canvas.style.setProperty('--image-annotate-content-bottom', `${insetBottom}px`);
this.canvas.style.setProperty('--image-annotate-content-left', `${insetLeft}px`);
}

this.viewOverlay = document.createElement('div');
this.viewOverlay.className = 'image-annotate-view';

Expand Down Expand Up @@ -321,7 +337,9 @@ export class AnnotateImage {
this.pendingRescale = true;
return;
}
this.applyRescale(renderedWidth, renderedHeight);
const contentWidth = renderedWidth - this.contentInset.left - this.contentInset.right;
const contentHeight = renderedHeight - this.contentInset.top - this.contentInset.bottom;
this.applyRescale(contentWidth, contentHeight);
}

/** Apply new scale factors and re-render all views. */
Expand All @@ -343,8 +361,10 @@ export class AnnotateImage {
if (!this.pendingRescale) return;
this.pendingRescale = false;
const rect = this.canvas.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
this.applyRescale(rect.width, rect.height);
const contentWidth = rect.width - this.contentInset.left - this.contentInset.right;
const contentHeight = rect.height - this.contentInset.top - this.contentInset.bottom;
if (contentWidth > 0 && contentHeight > 0) {
this.applyRescale(contentWidth, contentHeight);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/annotate-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class AnnotateView {
// Create the tooltip (was called "form" in old code)
this.tooltip = document.createElement('div');
this.tooltip.className = 'image-annotate-note';
this.tooltip.textContent = note.text;
this.tooltip.textContent = note.text.trim() ? note.text : '(no annotation)';
this.tooltip.style.display = 'none';
this.area.appendChild(this.tooltip);

Expand Down Expand Up @@ -86,7 +86,7 @@ export class AnnotateView {

/** Update the view's position, size, and text from the edit area after a save. */
resetPosition(editable: { area: HTMLElement; note: AnnotationNote }, text: string): void {
this.tooltip.textContent = text;
this.tooltip.textContent = text.trim() ? text : '(no annotation)';
this.tooltip.style.display = 'none';

// Position view DOM using the note's natural coordinates (already converted by edit)
Expand Down
10 changes: 6 additions & 4 deletions src/annotation.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
background-size: 20px 20px;
border: var(--image-annotate-add-border);
border-radius: var(--image-annotate-add-radius);
bottom: 8px;
bottom: calc(var(--image-annotate-content-bottom, 0px) + 8px);
color: #fff;
cursor: pointer;
height: 32px;
opacity: 0;
padding: 0;
position: absolute;
right: 8px;
right: calc(var(--image-annotate-content-right, 0px) + 8px);
transition:
opacity 0.2s,
background-color 0.2s;
Expand Down Expand Up @@ -80,7 +80,8 @@
}
.image-annotate-view {
display: none;
inset: 0;
inset: var(--image-annotate-content-top, 0px) var(--image-annotate-content-right, 0px)
var(--image-annotate-content-bottom, 0px) var(--image-annotate-content-left, 0px);
position: absolute;
}
@media (hover: hover) {
Expand Down Expand Up @@ -157,7 +158,8 @@
}
.image-annotate-edit {
display: none;
inset: 0;
inset: var(--image-annotate-content-top, 0px) var(--image-annotate-content-right, 0px)
var(--image-annotate-content-bottom, 0px) var(--image-annotate-content-left, 0px);
position: absolute;
}
.image-annotate-edit-form {
Expand Down
Loading
Loading