diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte
index 5115ab18f1..7cb8a5c076 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte
@@ -483,8 +483,6 @@
class="fixed start-0 top-0 grid size-full grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black"
use:focusTrap
bind:this={assetViewerHtmlElement}
- bind:clientWidth={containerWidth}
- bind:clientHeight={containerHeight}
>
{#if $slideshowState === SlideshowState.None && !assetViewerManager.isShowEditor}
@@ -526,7 +524,12 @@
{/if}
-
+
{#if viewerKind === 'StackVideoViewer'}
{
- const { offsetX, offsetY } = imageContentMetrics;
+ const { offsetX, offsetY, contentWidth } = imageContentMetrics;
- if (!canvas) {
+ if (!canvas || contentWidth === 0) {
return;
}
- canvas.setDimensions({
- width: containerWidth,
- height: containerHeight,
- });
+ if (!initialized) {
+ initialized = true;
+ canvas.setDimensions({ width: containerWidth, height: containerHeight });
- if (!faceRect) {
+ if (faceRect) {
+ faceRect.set({ top: offsetY + 200, left: offsetX + 200 });
+ faceRect.setCoords();
+ }
+
+ previousContentWidth = contentWidth;
+ previousOffsetX = offsetX;
+ previousOffsetY = offsetY;
+ positionFaceSelector();
return;
}
- faceRect.set({
- top: offsetY + 200,
- left: offsetX + 200,
- });
+ canvas.setDimensions({ width: containerWidth, height: containerHeight });
- faceRect.setCoords();
+ if (faceRect && previousContentWidth > 0) {
+ const scaled = scaleFaceRectOnResize(
+ { left: faceRect.left, top: faceRect.top, scaleX: faceRect.scaleX, scaleY: faceRect.scaleY },
+ { previousOffsetX, previousOffsetY, previousContentWidth },
+ offsetX,
+ offsetY,
+ contentWidth,
+ );
+ faceRect.set(scaled);
+ faceRect.setCoords();
+ }
+
+ previousContentWidth = contentWidth;
+ previousOffsetX = offsetX;
+ previousOffsetY = offsetY;
+
+ canvas.renderAll();
positionFaceSelector();
});
diff --git a/web/src/lib/utils/people-utils.spec.ts b/web/src/lib/utils/people-utils.spec.ts
index 80371bd9c4..89f79a8d13 100644
--- a/web/src/lib/utils/people-utils.spec.ts
+++ b/web/src/lib/utils/people-utils.spec.ts
@@ -1,6 +1,6 @@
import type { Faces } from '$lib/stores/people.store';
import type { ContentMetrics } from '$lib/utils/container-utils';
-import { getBoundingBox } from '$lib/utils/people-utils';
+import { getBoundingBox, scaleFaceRectOnResize, type FaceRectState, type ResizeContext } from '$lib/utils/people-utils';
const makeFace = (overrides: Partial = {}): Faces => ({
id: 'face-1',
@@ -97,3 +97,96 @@ describe('getBoundingBox', () => {
expect(boxes[0].left).toBeLessThan(boxes[1].left);
});
});
+
+describe('scaleFaceRectOnResize', () => {
+ const makeRect = (overrides: Partial = {}): FaceRectState => ({
+ left: 300,
+ top: 400,
+ scaleX: 1,
+ scaleY: 1,
+ ...overrides,
+ });
+
+ const makePrevious = (overrides: Partial = {}): ResizeContext => ({
+ previousOffsetX: 100,
+ previousOffsetY: 50,
+ previousContentWidth: 800,
+ ...overrides,
+ });
+
+ it('should preserve relative position when container doubles in size', () => {
+ const rect = makeRect({ left: 300, top: 250 });
+ const previous = makePrevious({ previousOffsetX: 100, previousOffsetY: 50, previousContentWidth: 800 });
+
+ const result = scaleFaceRectOnResize(rect, previous, 200, 100, 1600);
+
+ // imageRelLeft = (300 - 100) * 2 = 400, new left = 200 + 400 = 600
+ // imageRelTop = (250 - 50) * 2 = 400, new top = 100 + 400 = 500
+ expect(result.left).toBe(600);
+ expect(result.top).toBe(500);
+ expect(result.scaleX).toBe(2);
+ expect(result.scaleY).toBe(2);
+ });
+
+ it('should preserve relative position when container halves in size', () => {
+ const rect = makeRect({ left: 300, top: 250 });
+ const previous = makePrevious({ previousOffsetX: 100, previousOffsetY: 50, previousContentWidth: 800 });
+
+ const result = scaleFaceRectOnResize(rect, previous, 50, 25, 400);
+
+ // imageRelLeft = (300 - 100) * 0.5 = 100, new left = 50 + 100 = 150
+ // imageRelTop = (250 - 50) * 0.5 = 100, new top = 25 + 100 = 125
+ expect(result.left).toBe(150);
+ expect(result.top).toBe(125);
+ expect(result.scaleX).toBe(0.5);
+ expect(result.scaleY).toBe(0.5);
+ });
+
+ it('should handle no change in dimensions', () => {
+ const rect = makeRect({ left: 300, top: 250, scaleX: 1.5, scaleY: 1.5 });
+ const previous = makePrevious({ previousOffsetX: 100, previousOffsetY: 50, previousContentWidth: 800 });
+
+ const result = scaleFaceRectOnResize(rect, previous, 100, 50, 800);
+
+ expect(result.left).toBe(300);
+ expect(result.top).toBe(250);
+ expect(result.scaleX).toBe(1.5);
+ expect(result.scaleY).toBe(1.5);
+ });
+
+ it('should handle offset changes without content width change', () => {
+ const rect = makeRect({ left: 300, top: 250 });
+ const previous = makePrevious({ previousOffsetX: 100, previousOffsetY: 50, previousContentWidth: 800 });
+
+ const result = scaleFaceRectOnResize(rect, previous, 150, 75, 800);
+
+ // scale = 1, imageRelLeft = 200, imageRelTop = 200
+ // new left = 150 + 200 = 350, new top = 75 + 200 = 275
+ expect(result.left).toBe(350);
+ expect(result.top).toBe(275);
+ expect(result.scaleX).toBe(1);
+ expect(result.scaleY).toBe(1);
+ });
+
+ it('should compound existing scale factors', () => {
+ const rect = makeRect({ left: 300, top: 250, scaleX: 2, scaleY: 3 });
+ const previous = makePrevious({ previousContentWidth: 800 });
+
+ const result = scaleFaceRectOnResize(rect, previous, previous.previousOffsetX, previous.previousOffsetY, 1600);
+
+ expect(result.scaleX).toBe(4);
+ expect(result.scaleY).toBe(6);
+ });
+
+ it('should handle rect at image origin (top-left of content area)', () => {
+ const rect = makeRect({ left: 100, top: 50 });
+ const previous = makePrevious({ previousOffsetX: 100, previousOffsetY: 50, previousContentWidth: 800 });
+
+ const result = scaleFaceRectOnResize(rect, previous, 200, 100, 1600);
+
+ // imageRelLeft = (100 - 100) * 2 = 0, new left = 200
+ // imageRelTop = (50 - 50) * 2 = 0, new top = 100
+ expect(result.left).toBe(200);
+ expect(result.top).toBe(100);
+ });
+});
diff --git a/web/src/lib/utils/people-utils.ts b/web/src/lib/utils/people-utils.ts
index b8fb8973e6..81a76ee7f0 100644
--- a/web/src/lib/utils/people-utils.ts
+++ b/web/src/lib/utils/people-utils.ts
@@ -37,6 +37,38 @@ export const getBoundingBox = (faces: Faces[], metrics: ContentMetrics): Boundin
return boxes;
};
+export type FaceRectState = {
+ left: number;
+ top: number;
+ scaleX: number;
+ scaleY: number;
+};
+
+export type ResizeContext = {
+ previousOffsetX: number;
+ previousOffsetY: number;
+ previousContentWidth: number;
+};
+
+export const scaleFaceRectOnResize = (
+ faceRect: FaceRectState,
+ previous: ResizeContext,
+ newOffsetX: number,
+ newOffsetY: number,
+ newContentWidth: number,
+): FaceRectState => {
+ const scale = newContentWidth / previous.previousContentWidth;
+ const imageRelativeLeft = (faceRect.left - previous.previousOffsetX) * scale;
+ const imageRelativeTop = (faceRect.top - previous.previousOffsetY) * scale;
+
+ return {
+ left: newOffsetX + imageRelativeLeft,
+ top: newOffsetY + imageRelativeTop,
+ scaleX: faceRect.scaleX * scale,
+ scaleY: faceRect.scaleY * scale,
+ };
+};
+
export const zoomImageToBase64 = async (
face: AssetFaceResponseDto,
assetId: string,