Fix jitter

This commit is contained in:
Min Idzelis 2025-03-25 23:31:07 +00:00
parent 344c695b52
commit 6b0dcfffb8
2 changed files with 49 additions and 13 deletions

View File

@ -165,7 +165,13 @@
const updateIsScrolling = () => (assetStore.scrolling = true);
// note: don't throttle, debounch, or otherwise do this function async - it causes flicker
const updateSlidingWindow = () => assetStore.updateSlidingWindow(element?.scrollTop || 0);
const compensateScrollCallback = (delta: number) => element?.scrollBy(0, delta);
const compensateScrollCallback = ({ delta, top }: { delta?: number; top?: number }) => {
if (delta) {
element?.scrollBy(0, delta);
} else if (top) {
element?.scrollTo({ top });
}
};
const topSectionResizeObserver: OnResizeCallback = ({ height }) => (assetStore.topSectionHeight = height);
onMount(() => {

View File

@ -10,7 +10,7 @@ import {
import { formatDateGroupTitle, fromLocalDateTime } from '$lib/utils/timeline-util';
import { TUNABLES } from '$lib/utils/tunables';
import { getAssetInfo, getTimeBucket, getTimeBuckets, TimeBucketSize, type AssetResponseDto } from '@immich/sdk';
import { debounce, isEqual, throttle } from 'lodash-es';
import { clamp, debounce, isEqual, throttle } from 'lodash-es';
import { DateTime } from 'luxon';
import { t } from 'svelte-i18n';
@ -218,6 +218,7 @@ export type ViewportXY = Viewport & {
export class AssetBucket {
// --- public ---
#intersecting: boolean = $state(false);
actuallyIntersecting: boolean = $state(false);
isLoaded: boolean = $state(false);
dateGroups: AssetDateGroup[] = $state([]);
readonly store: AssetStore;
@ -233,6 +234,7 @@ export class AssetBucket {
#top: number = $state(0);
#initialCount: number = 0;
percent: number = $state(0);
// --- should be private, but is used by AssetStore ---
bucketCount: number = $derived(
@ -423,7 +425,7 @@ export class AssetBucket {
}
set bucketHeight(height: number) {
const { store } = this;
const { store, percent } = this;
const index = store.buckets.indexOf(this);
const bucketHeightDelta = height - this.#bucketHeight;
const prevBucket = store.buckets[index - 1];
@ -444,8 +446,14 @@ export class AssetBucket {
// if the bucket is 'before' the last intersecting bucket in the sliding window
// then adjust the scroll position by the delta, to compensate for the bucket
// size adjustment
if (currentIndex > 0 && index <= currentIndex) {
store.compensateScrollCallback?.(bucketHeightDelta);
if (currentIndex > 0) {
if (index < currentIndex) {
store.compensateScrollCallback?.({ delta: bucketHeightDelta });
} else if (currentIndex == currentIndex) {
this.store.updateIntersections();
const top = this.#top + height * percent;
store.compensateScrollCallback?.({ top });
}
}
}
}
@ -539,7 +547,7 @@ export class AssetStore {
scrubberTimelineHeight: number = $state(0);
// -- should be private, but used by AssetBucket
compensateScrollCallback: ((delta: number) => void) | undefined;
compensateScrollCallback: (({ delta, top }: { delta?: number; top?: number }) => void) | undefined;
topIntersectingBucket: AssetBucket | undefined = $state();
visibleWindow = $derived.by(() => ({
@ -704,29 +712,51 @@ export class AssetStore {
let topIntersectingBucket = undefined;
for (const bucket of this.buckets) {
this.#updateIntersection(bucket);
if (!topIntersectingBucket && bucket.intersecting) {
if (!topIntersectingBucket && bucket.actuallyIntersecting) {
topIntersectingBucket = bucket;
}
}
if (this.topIntersectingBucket !== topIntersectingBucket) {
this.topIntersectingBucket = topIntersectingBucket;
}
for (const bucket of this.buckets) {
if (bucket === this.topIntersectingBucket) {
this.topIntersectingBucket.percent = clamp(
(this.visibleWindow.top - this.topIntersectingBucket.top) / this.topIntersectingBucket.bucketHeight,
0,
1,
);
} else {
bucket.percent = 0;
}
}
}
#updateIntersection(bucket: AssetBucket) {
#calculateIntersecting(bucket: AssetBucket, expandTop: number, expandBottom: number) {
const bucketTop = bucket.top;
const bucketBottom = bucketTop + bucket.bucketHeight;
const topWindow = this.visibleWindow.top - INTERSECTION_EXPAND_TOP;
const bottomWindow = this.visibleWindow.bottom + INTERSECTION_EXPAND_BOTTOM;
const topWindow = this.visibleWindow.top - expandTop;
const bottomWindow = this.visibleWindow.bottom + expandBottom;
// a bucket intersections if
// 1) bucket's bottom is in the visible range -or-
// 2) bucket's bottom is in the visible range -or-
// 3) bucket's top is above visible range and bottom is below visible range
bucket.intersecting =
return (
(bucketTop >= topWindow && bucketTop < bottomWindow) ||
(bucketBottom >= topWindow && bucketBottom < bottomWindow) ||
(bucketTop < topWindow && bucketBottom >= bottomWindow);
(bucketTop < topWindow && bucketBottom >= bottomWindow)
);
}
#updateIntersection(bucket: AssetBucket) {
const actuallyIntersecting = this.#calculateIntersecting(bucket, 0, 0);
let preIntersecting = false;
if (!actuallyIntersecting) {
preIntersecting = this.#calculateIntersecting(bucket, INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM);
}
bucket.intersecting = actuallyIntersecting || preIntersecting;
bucket.actuallyIntersecting = actuallyIntersecting;
}
#processPendingChanges = throttle(() => {
@ -743,7 +773,7 @@ export class AssetStore {
this.#pendingChanges = [];
}, 2500);
setCompensateScrollCallback(compensateScrollCallback?: (delta: number) => void) {
setCompensateScrollCallback(compensateScrollCallback?: ({ delta, top }: { delta?: number; top?: number }) => void) {
this.compensateScrollCallback = compensateScrollCallback;
}