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); const updateIsScrolling = () => (assetStore.scrolling = true);
// note: don't throttle, debounch, or otherwise do this function async - it causes flicker // note: don't throttle, debounch, or otherwise do this function async - it causes flicker
const updateSlidingWindow = () => assetStore.updateSlidingWindow(element?.scrollTop || 0); 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); const topSectionResizeObserver: OnResizeCallback = ({ height }) => (assetStore.topSectionHeight = height);
onMount(() => { onMount(() => {

View File

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