immich/web/src/lib/utils/container-utils.ts
midzelis 7e2863858d feat(web): adaptive progressive image loading for photo viewer
Replace ImageManager with a new AdaptiveImageLoader that progressively
loads images through quality tiers (thumbnail → preview → original).

New components and utilities:
- AdaptiveImage: layered image renderer with thumbhash, thumbnail,
  preview, and original layers with visibility managed by load state
- AdaptiveImageLoader: state machine driving the quality progression
  with per-quality callbacks and error handling
- ImageLayer/Image: low-level image elements with load/error lifecycle
- PreloadManager: preloads adjacent assets for instant navigation
- AlphaBackground/DelayedLoadingSpinner: loading state UI

Zoom is handled via a derived CSS transform applied to the content
wrapper in AdaptiveImage, with the zoom library (zoomTarget: null)
only tracking state without manipulating the DOM directly.

Also adds scaleToCover to container-utils and getAssetUrls to utils.
2026-03-09 13:34:48 +00:00

59 lines
1.9 KiB
TypeScript

export interface ContentMetrics {
contentWidth: number;
contentHeight: number;
offsetX: number;
offsetY: number;
}
export const scaleToCover = (
dimensions: { width: number; height: number },
container: { width: number; height: number },
): { width: number; height: number } => {
const scaleX = container.width / dimensions.width;
const scaleY = container.height / dimensions.height;
const scale = Math.max(scaleX, scaleY);
return {
width: dimensions.width * scale,
height: dimensions.height * scale,
};
};
export const scaleToFit = (
dimensions: { width: number; height: number },
container: { width: number; height: number },
): { width: number; height: number } => {
const scaleX = container.width / dimensions.width;
const scaleY = container.height / dimensions.height;
const scale = Math.min(scaleX, scaleY);
return {
width: dimensions.width * scale,
height: dimensions.height * scale,
};
};
const getElementSize = (element: HTMLImageElement | HTMLVideoElement): { width: number; height: number } => {
if (element instanceof HTMLVideoElement) {
return { width: element.clientWidth, height: element.clientHeight };
}
return { width: element.width, height: element.height };
};
export const getNaturalSize = (element: HTMLImageElement | HTMLVideoElement): { width: number; height: number } => {
if (element instanceof HTMLVideoElement) {
return { width: element.videoWidth, height: element.videoHeight };
}
return { width: element.naturalWidth, height: element.naturalHeight };
};
export const getContentMetrics = (element: HTMLImageElement | HTMLVideoElement): ContentMetrics => {
const natural = getNaturalSize(element);
const client = getElementSize(element);
const { width: contentWidth, height: contentHeight } = scaleToFit(natural, client);
return {
contentWidth,
contentHeight,
offsetX: (client.width - contentWidth) / 2,
offsetY: (client.height - contentHeight) / 2,
};
};