diff --git a/web/src/lib/components/timeline/AssetLayout.svelte b/web/src/lib/components/timeline/AssetLayout.svelte
index 88f03ef46a..d67e47aea8 100644
--- a/web/src/lib/components/timeline/AssetLayout.svelte
+++ b/web/src/lib/components/timeline/AssetLayout.svelte
@@ -12,6 +12,8 @@
type Props = {
viewerAssets: ViewerAsset[];
+ firstActiveIndex: number;
+ lastActiveIndex: number;
width: number;
height: number;
manager: VirtualScrollManager;
@@ -26,18 +28,27 @@
customThumbnailLayout?: Snippet<[asset: TimelineAsset]>;
};
- const { viewerAssets, width, height, manager, thumbnail, customThumbnailLayout }: Props = $props();
+ const {
+ viewerAssets,
+ firstActiveIndex,
+ lastActiveIndex,
+ width,
+ height,
+ manager,
+ thumbnail,
+ customThumbnailLayout,
+ }: Props = $props();
const transitionDuration = $derived(manager.suspendTransitions && !$isUploading ? 0 : 150);
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
-
- const firstInOrNearViewport = $derived(viewerAssets.findIndex((a) => a.isInOrNearViewport));
- const lastInOrNearViewport = $derived(viewerAssets.findLastIndex((a) => a.isInOrNearViewport));
+ const visibleViewerAssets = $derived(
+ firstActiveIndex === -1 ? [] : viewerAssets.slice(firstActiveIndex, lastActiveIndex + 1),
+ );
- {#each viewerAssets.slice(firstInOrNearViewport, lastInOrNearViewport + 1) as viewerAsset (viewerAsset.id)}
+ {#each visibleViewerAssets as viewerAsset (viewerAsset.id)}
{@const position = viewerAsset.position!}
{@const asset = viewerAsset.asset!}
diff --git a/web/src/lib/components/timeline/Month.svelte b/web/src/lib/components/timeline/Month.svelte
index 3f4b9b0661..7d1b223bfc 100644
--- a/web/src/lib/components/timeline/Month.svelte
+++ b/web/src/lib/components/timeline/Month.svelte
@@ -101,6 +101,8 @@
number): number {
+ let lo = 0;
+ let hi = assets.length;
+ while (lo < hi) {
+ const mid = (lo + hi) >>> 1;
+ if (key(assets[mid].position!) < target) {
+ lo = mid + 1;
+ } else {
+ hi = mid;
+ }
+ }
+ return lo;
+}
+
export class TimelineDay {
readonly timelineMonth: TimelineMonth;
readonly index: number;
@@ -18,12 +37,16 @@ export class TimelineDay {
height = $state(0);
width = $state(0);
+ // Assets in or near the viewport; active assets should be added to the DOM.
+ firstActiveIndex = $state(-1);
+ lastActiveIndex = $state(-1);
+ isInOrNearViewport = $derived(this.firstActiveIndex !== -1);
+
#top: number = $state(0);
#start: number = $state(0);
#row = $state(0);
#col = $state(0);
#deferredLayout = false;
- #lastInOrNearViewport = -1;
constructor(timelineMonth: TimelineMonth, index: number, day: number, groupTitle: string, orderBy: AssetOrderBy) {
this.index = index;
@@ -149,18 +172,32 @@ export class TimelineDay {
for (let i = 0; i < this.viewerAssets.length; i++) {
this.viewerAssets[i].position = geometry.getPosition(i);
}
+ this.updateAssetBoundaries();
+ }
+
+ updateAssetBoundaries() {
+ const manager = this.timelineMonth.timelineManager;
+ const visibleWindow = manager.visibleWindow;
+ if (this.viewerAssets.length === 0 || !this.viewerAssets[0].position) {
+ this.firstActiveIndex = -1;
+ this.lastActiveIndex = -1;
+ return;
+ }
+
+ const dayOffset = this.absoluteTimelineDayTop;
+ const headerHeight = manager.headerHeight;
+ const expandedTop = visibleWindow.top - headerHeight - INTERSECTION_EXPAND_TOP - dayOffset;
+ const expandedBottom = visibleWindow.bottom + headerHeight + INTERSECTION_EXPAND_BOTTOM - dayOffset;
+
+ const first = lowerBound(this.viewerAssets, expandedTop, (p) => p.top + p.height);
+ const last = lowerBound(this.viewerAssets, expandedBottom, (p) => p.top) - 1;
+
+ const hasActive = last >= first && first < this.viewerAssets.length;
+ this.firstActiveIndex = hasActive ? first : -1;
+ this.lastActiveIndex = hasActive ? last : -1;
}
get absoluteTimelineDayTop() {
return this.timelineMonth.top + this.#top;
}
-
- get isInOrNearViewport() {
- if (this.#lastInOrNearViewport !== -1 && this.viewerAssets[this.#lastInOrNearViewport].isInOrNearViewport) {
- return true;
- }
-
- this.#lastInOrNearViewport = this.viewerAssets.findIndex((viewAsset) => viewAsset.isInOrNearViewport);
- return this.#lastInOrNearViewport !== -1;
- }
}
diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts
index 32e6d90d1c..5ac2deaa54 100644
--- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts
@@ -214,6 +214,11 @@ export class TimelineManager extends VirtualScrollManager {
for (const month of this.months) {
updateTimelineMonthViewportProximity(this, month);
+ if (month.isInOrNearViewport && month.isLoaded) {
+ for (const day of month.timelineDays) {
+ day.updateAssetBoundaries();
+ }
+ }
}
const month = this.months.find((month) => month.isInViewport);
diff --git a/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts
index bee53151f7..d45c6203cd 100644
--- a/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts
@@ -254,7 +254,7 @@ export class TimelineMonth {
addContext.newTimelineDays.add(timelineDay);
}
- const viewerAsset = new ViewerAsset(timelineDay, timelineAsset);
+ const viewerAsset = new ViewerAsset(timelineAsset);
timelineDay.viewerAssets.push(viewerAsset);
addContext.changedTimelineDays.add(timelineDay);
}
diff --git a/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts b/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts
index 0179dd9e74..3a4ed4545c 100644
--- a/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts
@@ -1,36 +1,12 @@
import type { CommonPosition } from '$lib/utils/layout-utils';
-import {
- ViewportProximity,
- calculateViewerAssetViewportProximity,
- isInOrNearViewport,
-} from './internal/intersection-support.svelte';
-import type { TimelineDay } from './timeline-day.svelte';
import type { TimelineAsset } from './types';
export class ViewerAsset {
- readonly #group: TimelineDay;
-
- #viewportProximity = $derived.by(() => {
- if (!this.position) {
- return ViewportProximity.FarFromViewport;
- }
-
- const store = this.#group.timelineMonth.timelineManager;
- const positionTop = this.#group.absoluteTimelineDayTop + this.position.top;
-
- return calculateViewerAssetViewportProximity(store, positionTop, this.position.height);
- });
-
- get isInOrNearViewport() {
- return isInOrNearViewport(this.#viewportProximity);
- }
-
position: CommonPosition | undefined = $state.raw();
asset: TimelineAsset = $state() as TimelineAsset;
id: string = $derived(this.asset.id);
- constructor(group: TimelineDay, asset: TimelineAsset) {
- this.#group = group;
+ constructor(asset: TimelineAsset) {
this.asset = asset;
}
}