mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	use svelte motion tweening for animation (#2788)
It look like Svelte has a concept of 'tweening' for writing animations, which should reduce the complexity of the animation code. Thanks to @probablykasper for finding this. A lot of the logic has been rewritten for reactivity, which further reduces complexity.
This commit is contained in:
		@@ -1,5 +1,4 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import { browser } from '$app/environment';
 | 
					 | 
				
			||||||
	import { memoryStore } from '$lib/stores/memory.store';
 | 
						import { memoryStore } from '$lib/stores/memory.store';
 | 
				
			||||||
	import { DateTime } from 'luxon';
 | 
						import { DateTime } from 'luxon';
 | 
				
			||||||
	import { onDestroy, onMount } from 'svelte';
 | 
						import { onDestroy, onMount } from 'svelte';
 | 
				
			||||||
@@ -19,36 +18,58 @@
 | 
				
			|||||||
	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
						import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
	import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
 | 
						import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
 | 
				
			||||||
	import { fade } from 'svelte/transition';
 | 
						import { fade } from 'svelte/transition';
 | 
				
			||||||
 | 
						import { tweened } from 'svelte/motion';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let memoryIndex: number;
 | 
						const parseIndex = (s: string | null, max: number | null) =>
 | 
				
			||||||
	$: {
 | 
							Math.max(Math.min(parseInt(s ?? '') || 0, max ?? 0), 0);
 | 
				
			||||||
		const index = parseInt($page.url.searchParams.get('memory') ?? '') || 0;
 | 
					 | 
				
			||||||
		memoryIndex = index < $memoryStore?.length ? index : 0;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$: previousMemory = $memoryStore?.[memoryIndex - 1] || null;
 | 
						$: memoryIndex = parseIndex($page.url.searchParams.get('memory'), $memoryStore?.length - 1);
 | 
				
			||||||
	$: currentMemory = $memoryStore?.[memoryIndex] || null;
 | 
						$: assetIndex = parseIndex($page.url.searchParams.get('asset'), currentMemory?.assets.length - 1);
 | 
				
			||||||
	$: nextMemory = $memoryStore?.[memoryIndex + 1] || null;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let assetIndex: number;
 | 
						$: previousMemory = $memoryStore?.[memoryIndex - 1];
 | 
				
			||||||
	$: {
 | 
						$: currentMemory = $memoryStore?.[memoryIndex];
 | 
				
			||||||
		const index = parseInt($page.url.searchParams.get('asset') ?? '') || 0;
 | 
						$: nextMemory = $memoryStore?.[memoryIndex + 1];
 | 
				
			||||||
		assetIndex = index < currentMemory?.assets.length ? index : 0;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$: previousAsset = currentMemory?.assets[assetIndex - 1] || null;
 | 
						$: previousAsset = currentMemory?.assets[assetIndex - 1];
 | 
				
			||||||
	$: currentAsset = currentMemory?.assets[assetIndex] || null;
 | 
						$: currentAsset = currentMemory?.assets[assetIndex];
 | 
				
			||||||
	$: nextAsset = currentMemory?.assets[assetIndex + 1] || null;
 | 
						$: nextAsset = currentMemory?.assets[assetIndex + 1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$: canAdvance = !!(nextMemory || nextAsset);
 | 
						$: canAdvance = !!(nextMemory || nextAsset);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$: if (!canAdvance && browser) {
 | 
						const toNextMemory = () => goto(`?memory=${memoryIndex + 1}`);
 | 
				
			||||||
		pause();
 | 
						const toPreviousMemory = () => goto(`?memory=${memoryIndex - 1}`);
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let memoryGallery: HTMLElement;
 | 
						const toNextAsset = () => goto(`?memory=${memoryIndex}&asset=${assetIndex + 1}`);
 | 
				
			||||||
	let memoryWrapper: HTMLElement;
 | 
						const toPreviousAsset = () => goto(`?memory=${memoryIndex}&asset=${assetIndex - 1}`);
 | 
				
			||||||
	let galleryInView = false;
 | 
					
 | 
				
			||||||
 | 
						const toNext = () => (nextAsset ? toNextAsset() : toNextMemory());
 | 
				
			||||||
 | 
						const toPrevious = () => (previousAsset ? toPreviousAsset() : toPreviousMemory());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const progress = tweened<number>(0, {
 | 
				
			||||||
 | 
							duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0)
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const play = () => progress.set(1);
 | 
				
			||||||
 | 
						const pause = () => progress.set($progress);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let paused = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Play or pause progress when the paused state changes.
 | 
				
			||||||
 | 
						$: paused ? pause() : play();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Progress should be paused when it's no longer possible to advance.
 | 
				
			||||||
 | 
						$: paused ||= !canAdvance;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Advance to the next asset or memory when progress is complete.
 | 
				
			||||||
 | 
						$: $progress === 1 && toNext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Progress should be resumed when reset and not paused.
 | 
				
			||||||
 | 
						$: !$progress && !paused && play();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Progress should be reset when the current memory or asset changes.
 | 
				
			||||||
 | 
						$: memoryIndex, assetIndex, progress.set(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onDestroy(() => pause());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	onMount(async () => {
 | 
						onMount(async () => {
 | 
				
			||||||
		if (!$memoryStore) {
 | 
							if (!$memoryStore) {
 | 
				
			||||||
@@ -59,67 +80,9 @@
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	onDestroy(() => browser && pause());
 | 
						let memoryGallery: HTMLElement;
 | 
				
			||||||
 | 
						let memoryWrapper: HTMLElement;
 | 
				
			||||||
	const toPreviousMemory = () => previousMemory && goto(`?memory=${memoryIndex - 1}`);
 | 
						let galleryInView = false;
 | 
				
			||||||
 | 
					 | 
				
			||||||
	const toNextMemory = () => nextMemory && goto(`?memory=${memoryIndex + 1}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const toPreviousAsset = () =>
 | 
					 | 
				
			||||||
		previousAsset ? goto(`?memory=${memoryIndex}&asset=${assetIndex - 1}`) : toPreviousMemory();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const toNextAsset = () =>
 | 
					 | 
				
			||||||
		nextAsset ? goto(`?memory=${memoryIndex}&asset=${assetIndex + 1}`) : toNextMemory();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const duration = 5000; // 5 seconds
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	let paused = true;
 | 
					 | 
				
			||||||
	let progress = 0;
 | 
					 | 
				
			||||||
	let animationFrameRequest: number;
 | 
					 | 
				
			||||||
	let start: number | null = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const requestDraw = () => (animationFrameRequest = requestAnimationFrame(draw));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const draw = (now: number) => {
 | 
					 | 
				
			||||||
		requestDraw();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		start ??= now - progress * duration;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const elapsed = now - start;
 | 
					 | 
				
			||||||
		progress = Math.min(1, elapsed / duration);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (progress !== 1) {
 | 
					 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		toNextAsset();
 | 
					 | 
				
			||||||
		start = now;
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const play = () => {
 | 
					 | 
				
			||||||
		if (!canAdvance) {
 | 
					 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		paused = false;
 | 
					 | 
				
			||||||
		requestDraw();
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const pause = () => {
 | 
					 | 
				
			||||||
		paused = true;
 | 
					 | 
				
			||||||
		cancelAnimationFrame(animationFrameRequest);
 | 
					 | 
				
			||||||
		resetStart();
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const resetProgress = () => {
 | 
					 | 
				
			||||||
		progress = 0;
 | 
					 | 
				
			||||||
		resetStart();
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const resetStart = () => (start = null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Progress should be reset when the current memory or asset changes.
 | 
					 | 
				
			||||||
	$: memoryIndex, assetIndex, resetProgress();
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}>
 | 
					<section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}>
 | 
				
			||||||
@@ -136,12 +99,12 @@
 | 
				
			|||||||
					<CircleIconButton
 | 
										<CircleIconButton
 | 
				
			||||||
						logo={paused ? Play : Pause}
 | 
											logo={paused ? Play : Pause}
 | 
				
			||||||
						forceDark
 | 
											forceDark
 | 
				
			||||||
						on:click={paused ? play : pause}
 | 
											on:click={() => (paused = !paused)}
 | 
				
			||||||
					/>
 | 
										/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					<div class="relative w-full">
 | 
										<div class="relative w-full">
 | 
				
			||||||
						<span class="absolute left-0 w-full h-[2px] bg-gray-500" />
 | 
											<span class="absolute left-0 w-full h-[2px] bg-gray-500" />
 | 
				
			||||||
						<span class="absolute left-0 h-[2px] bg-white" style:width={`${progress * 100}%`} />
 | 
											<span class="absolute left-0 h-[2px] bg-white" style:width={`${$progress * 100}%`} />
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					<div>
 | 
										<div>
 | 
				
			||||||
@@ -215,7 +178,7 @@
 | 
				
			|||||||
										<CircleIconButton
 | 
															<CircleIconButton
 | 
				
			||||||
											logo={ChevronLeft}
 | 
																logo={ChevronLeft}
 | 
				
			||||||
											backgroundColor="#202123"
 | 
																backgroundColor="#202123"
 | 
				
			||||||
											on:click={toPreviousAsset}
 | 
																on:click={toPrevious}
 | 
				
			||||||
										/>
 | 
															/>
 | 
				
			||||||
									{/if}
 | 
														{/if}
 | 
				
			||||||
								</div>
 | 
													</div>
 | 
				
			||||||
@@ -226,7 +189,7 @@
 | 
				
			|||||||
										<CircleIconButton
 | 
															<CircleIconButton
 | 
				
			||||||
											logo={ChevronRight}
 | 
																logo={ChevronRight}
 | 
				
			||||||
											backgroundColor="#202123"
 | 
																backgroundColor="#202123"
 | 
				
			||||||
											on:click={toNextAsset}
 | 
																on:click={toNext}
 | 
				
			||||||
										/>
 | 
															/>
 | 
				
			||||||
									{/if}
 | 
														{/if}
 | 
				
			||||||
								</div>
 | 
													</div>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user