mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
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
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
import { getNaturalSize, scaleToFit, type ContentMetrics } from '$lib/utils/container-utils';
|
import { getNaturalSize, scaleToFit, type ContentMetrics } from '$lib/utils/container-utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getOcrBoundingBoxes } from '$lib/utils/ocr-utils';
|
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 { type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { toastManager } from '@immich/ui';
|
import { toastManager } from '@immich/ui';
|
||||||
import { onDestroy, untrack } from 'svelte';
|
import { onDestroy, untrack } from 'svelte';
|
||||||
@@ -82,6 +82,18 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const highlightedBoxes = $derived(getBoundingBox($boundingBoxesArray, overlayMetrics));
|
||||||
|
const isHighlighting = $derived(highlightedBoxes.length > 0);
|
||||||
|
|
||||||
|
let visibleBoxes = $state<BoundingBox[]>([]);
|
||||||
|
let visibleBoundingBoxes = $state<Faces[]>([]);
|
||||||
|
$effect(() => {
|
||||||
|
if (isHighlighting) {
|
||||||
|
visibleBoxes = highlightedBoxes;
|
||||||
|
visibleBoundingBoxes = $boundingBoxesArray;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const ocrBoxes = $derived(ocrManager.showOverlay ? getOcrBoundingBoxes(ocrManager.data, overlayMetrics) : []);
|
const ocrBoxes = $derived(ocrManager.showOverlay ? getOcrBoundingBoxes(ocrManager.data, overlayMetrics) : []);
|
||||||
|
|
||||||
const onCopy = async () => {
|
const onCopy = async () => {
|
||||||
@@ -242,21 +254,37 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet overlays()}
|
{#snippet overlays()}
|
||||||
{#each getBoundingBox($boundingBoxesArray, overlayMetrics) as boundingbox, index (boundingbox.id)}
|
<div
|
||||||
<div
|
class="absolute inset-0 pointer-events-none transition-opacity duration-150"
|
||||||
class="absolute border-solid border-white border-3 rounded-lg"
|
style:opacity={isHighlighting ? 1 : 0}
|
||||||
style="top: {boundingbox.top}px; left: {boundingbox.left}px; height: {boundingbox.height}px; width: {boundingbox.width}px;"
|
>
|
||||||
></div>
|
<svg class="absolute inset-0 w-full h-full">
|
||||||
{#if faceToNameMap.get($boundingBoxesArray[index])}
|
<defs>
|
||||||
|
<mask id="face-dim-mask">
|
||||||
|
<rect width="100%" height="100%" fill="white" />
|
||||||
|
{#each visibleBoxes as box (box.id)}
|
||||||
|
<rect x={box.left} y={box.top} width={box.width} height={box.height} fill="black" rx="8" />
|
||||||
|
{/each}
|
||||||
|
</mask>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" fill="rgba(0,0,0,0.4)" mask="url(#face-dim-mask)" />
|
||||||
|
</svg>
|
||||||
|
{#each visibleBoxes as boundingbox, index (boundingbox.id)}
|
||||||
<div
|
<div
|
||||||
class="absolute bg-white/90 text-black px-2 py-1 rounded text-sm font-medium whitespace-nowrap pointer-events-none shadow-lg"
|
class="absolute border-solid border-white border-3 rounded-lg"
|
||||||
style="top: {boundingbox.top + boundingbox.height + 4}px; left: {boundingbox.left +
|
style="top: {boundingbox.top}px; left: {boundingbox.left}px; height: {boundingbox.height}px; width: {boundingbox.width}px;"
|
||||||
boundingbox.width}px; transform: translateX(-100%);"
|
></div>
|
||||||
>
|
{#if faceToNameMap.get(visibleBoundingBoxes[index])}
|
||||||
{faceToNameMap.get($boundingBoxesArray[index])}
|
<div
|
||||||
</div>
|
class="absolute bg-white/90 text-black px-2 py-1 rounded text-sm font-medium whitespace-nowrap pointer-events-none shadow-lg"
|
||||||
{/if}
|
style="top: {boundingbox.top + boundingbox.height + 4}px; left: {boundingbox.left +
|
||||||
{/each}
|
boundingbox.width}px; transform: translateX(-100%);"
|
||||||
|
>
|
||||||
|
{faceToNameMap.get(visibleBoundingBoxes[index])}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#each ocrBoxes as ocrBox (ocrBox.id)}
|
{#each ocrBoxes as ocrBox (ocrBox.id)}
|
||||||
<OcrBoundingBox {ocrBox} />
|
<OcrBoundingBox {ocrBox} />
|
||||||
|
|||||||
Reference in New Issue
Block a user