From ce346bf95661fd126d10b4d1136e40caacc799eb Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Mon, 30 Mar 2026 17:16:38 -0400 Subject: [PATCH] feat(web): dim photo outside hovered face bounding box (#27402) When hovering over a detected face in the photo viewer, an SVG mask overlay dims the rest of the image (40% black) while leaving the hovered face fully visible. The overlay fades in/out smoothly via CSS opacity transition by freezing the last highlighted box positions in state, preventing the overlay from popping off instantly when the mouse leaves. Change-Id: I07e2eb2b297820ec89812785fe7943846a6a6964 --- .../asset-viewer/photo-viewer.svelte | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index a26b66b0b6..a42eab6c4f 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -16,7 +16,7 @@ import { getNaturalSize, scaleToFit, type ContentMetrics } from '$lib/utils/container-utils'; import { handleError } from '$lib/utils/handle-error'; import { getOcrBoundingBoxes } from '$lib/utils/ocr-utils'; - import { getBoundingBox } from '$lib/utils/people-utils'; + import { getBoundingBox, type BoundingBox } from '$lib/utils/people-utils'; import { type SharedLinkResponseDto } from '@immich/sdk'; import { toastManager } from '@immich/ui'; import { onDestroy, untrack } from 'svelte'; @@ -82,6 +82,18 @@ }; }); + const highlightedBoxes = $derived(getBoundingBox($boundingBoxesArray, overlayMetrics)); + const isHighlighting = $derived(highlightedBoxes.length > 0); + + let visibleBoxes = $state([]); + let visibleBoundingBoxes = $state([]); + $effect(() => { + if (isHighlighting) { + visibleBoxes = highlightedBoxes; + visibleBoundingBoxes = $boundingBoxesArray; + } + }); + const ocrBoxes = $derived(ocrManager.showOverlay ? getOcrBoundingBoxes(ocrManager.data, overlayMetrics) : []); const onCopy = async () => { @@ -242,21 +254,37 @@ {/if} {/snippet} {#snippet overlays()} - {#each getBoundingBox($boundingBoxesArray, overlayMetrics) as boundingbox, index (boundingbox.id)} -
- {#if faceToNameMap.get($boundingBoxesArray[index])} +
+ + + + + {#each visibleBoxes as box (box.id)} + + {/each} + + + + + {#each visibleBoxes as boundingbox, index (boundingbox.id)}
- {faceToNameMap.get($boundingBoxesArray[index])} -
- {/if} - {/each} + class="absolute border-solid border-white border-3 rounded-lg" + style="top: {boundingbox.top}px; left: {boundingbox.left}px; height: {boundingbox.height}px; width: {boundingbox.width}px;" + >
+ {#if faceToNameMap.get(visibleBoundingBoxes[index])} +
+ {faceToNameMap.get(visibleBoundingBoxes[index])} +
+ {/if} + {/each} + {#each ocrBoxes as ocrBox (ocrBox.id)}