mirror of
https://github.com/immich-app/immich.git
synced 2026-03-26 20:00:44 +03:00
fix(web): skip thumbhash fade for offscreen-loaded thumbnails
Thumbnails that finish loading while offscreen (in the 500px pre-load zone) were still playing the thumbhash fade-out transition, causing visual flicker when scrolling. This adds an `actuallyIntersecting` property (zero-margin viewport check) alongside the existing `intersecting` (500px expanded margins) to distinguish between pre-loaded and truly visible assets. - Refactor `calculateViewerAssetIntersecting` to return a numeric flag (NONE/PRE/ACTUAL) avoiding object allocation in the hot derived path - Inline `calculateMonthGroupIntersecting` into `updateIntersectionMonthGroup` to avoid intermediate object allocation - Thread `actuallyIntersecting` through AssetLayout → Month → Timeline → Thumbnail snippet chain - Use `actuallyIntersecting` in Thumbnail to skip the fade when loaded offscreen - Add unit tests for `isIntersecting` and `calculateViewerAssetIntersecting`
This commit is contained in:
@@ -6,31 +6,8 @@ const {
|
||||
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
|
||||
} = TUNABLES;
|
||||
|
||||
export function updateIntersectionMonthGroup(timelineManager: TimelineManager, month: MonthGroup) {
|
||||
const actuallyIntersecting = calculateMonthGroupIntersecting(timelineManager, month, 0, 0);
|
||||
let preIntersecting = false;
|
||||
if (!actuallyIntersecting) {
|
||||
preIntersecting = calculateMonthGroupIntersecting(
|
||||
timelineManager,
|
||||
month,
|
||||
INTERSECTION_EXPAND_TOP,
|
||||
INTERSECTION_EXPAND_BOTTOM,
|
||||
);
|
||||
}
|
||||
month.intersecting = actuallyIntersecting || preIntersecting;
|
||||
month.actuallyIntersecting = actuallyIntersecting;
|
||||
if (preIntersecting || actuallyIntersecting) {
|
||||
timelineManager.clearDeferredLayout(month);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* General function to check if a rectangular region intersects with a window.
|
||||
* @param regionTop - Top position of the region to check
|
||||
* @param regionBottom - Bottom position of the region to check
|
||||
* @param windowTop - Top position of the window
|
||||
* @param windowBottom - Bottom position of the window
|
||||
* @returns true if the region intersects with the window
|
||||
*/
|
||||
export function isIntersecting(regionTop: number, regionBottom: number, windowTop: number, windowBottom: number) {
|
||||
return (
|
||||
@@ -40,34 +17,65 @@ export function isIntersecting(regionTop: number, regionBottom: number, windowTo
|
||||
);
|
||||
}
|
||||
|
||||
export function calculateMonthGroupIntersecting(
|
||||
timelineManager: TimelineManager,
|
||||
monthGroup: MonthGroup,
|
||||
expandTop: number,
|
||||
expandBottom: number,
|
||||
) {
|
||||
const monthGroupTop = monthGroup.top;
|
||||
const monthGroupBottom = monthGroupTop + monthGroup.height;
|
||||
const topWindow = timelineManager.visibleWindow.top - expandTop;
|
||||
const bottomWindow = timelineManager.visibleWindow.bottom + expandBottom;
|
||||
export function updateIntersectionMonthGroup(timelineManager: TimelineManager, month: MonthGroup) {
|
||||
const monthGroupTop = month.top;
|
||||
const monthGroupBottom = monthGroupTop + month.height;
|
||||
const windowTop = timelineManager.visibleWindow.top;
|
||||
const windowBottom = timelineManager.visibleWindow.bottom;
|
||||
|
||||
return isIntersecting(monthGroupTop, monthGroupBottom, topWindow, bottomWindow);
|
||||
const actuallyIntersecting = isIntersecting(monthGroupTop, monthGroupBottom, windowTop, windowBottom);
|
||||
|
||||
let intersecting = actuallyIntersecting;
|
||||
if (!actuallyIntersecting) {
|
||||
intersecting = isIntersecting(
|
||||
monthGroupTop,
|
||||
monthGroupBottom,
|
||||
windowTop - INTERSECTION_EXPAND_TOP,
|
||||
windowBottom + INTERSECTION_EXPAND_BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
month.intersecting = intersecting;
|
||||
month.actuallyIntersecting = actuallyIntersecting;
|
||||
if (intersecting) {
|
||||
timelineManager.clearDeferredLayout(month);
|
||||
}
|
||||
}
|
||||
|
||||
// Bit flags for intersection state
|
||||
export const Intersection = {
|
||||
NONE: 0,
|
||||
PRE: 1,
|
||||
ACTUAL: 3, // includes PRE (both bits set)
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Calculate intersection for viewer assets with additional parameters like header height
|
||||
* Returns a numeric flag: NONE (0), PRE (1, within expanded margin only), or ACTUAL (3, truly visible).
|
||||
*/
|
||||
export function calculateViewerAssetIntersecting(
|
||||
timelineManager: TimelineManager,
|
||||
positionTop: number,
|
||||
positionHeight: number,
|
||||
expandTop: number = INTERSECTION_EXPAND_TOP,
|
||||
expandBottom: number = INTERSECTION_EXPAND_BOTTOM,
|
||||
) {
|
||||
const topWindow = timelineManager.visibleWindow.top - timelineManager.headerHeight - expandTop;
|
||||
const bottomWindow = timelineManager.visibleWindow.bottom + timelineManager.headerHeight + expandBottom;
|
||||
|
||||
const positionBottom = positionTop + positionHeight;
|
||||
const headerHeight = timelineManager.headerHeight;
|
||||
const windowTop = timelineManager.visibleWindow.top - headerHeight;
|
||||
const windowBottom = timelineManager.visibleWindow.bottom + headerHeight;
|
||||
|
||||
return isIntersecting(positionTop, positionBottom, topWindow, bottomWindow);
|
||||
if (isIntersecting(positionTop, positionBottom, windowTop, windowBottom)) {
|
||||
return Intersection.ACTUAL;
|
||||
}
|
||||
|
||||
if (
|
||||
isIntersecting(
|
||||
positionTop,
|
||||
positionBottom,
|
||||
windowTop - INTERSECTION_EXPAND_TOP,
|
||||
windowBottom + INTERSECTION_EXPAND_BOTTOM,
|
||||
)
|
||||
) {
|
||||
return Intersection.PRE;
|
||||
}
|
||||
|
||||
return Intersection.NONE;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user