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"> | ||||
| 	import { browser } from '$app/environment'; | ||||
| 	import { memoryStore } from '$lib/stores/memory.store'; | ||||
| 	import { DateTime } from 'luxon'; | ||||
| 	import { onDestroy, onMount } from 'svelte'; | ||||
| @@ -19,36 +18,58 @@ | ||||
| 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||
| 	import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import { tweened } from 'svelte/motion'; | ||||
|  | ||||
| 	let memoryIndex: number; | ||||
| 	$: { | ||||
| 		const index = parseInt($page.url.searchParams.get('memory') ?? '') || 0; | ||||
| 		memoryIndex = index < $memoryStore?.length ? index : 0; | ||||
| 	} | ||||
| 	const parseIndex = (s: string | null, max: number | null) => | ||||
| 		Math.max(Math.min(parseInt(s ?? '') || 0, max ?? 0), 0); | ||||
|  | ||||
| 	$: previousMemory = $memoryStore?.[memoryIndex - 1] || null; | ||||
| 	$: currentMemory = $memoryStore?.[memoryIndex] || null; | ||||
| 	$: nextMemory = $memoryStore?.[memoryIndex + 1] || null; | ||||
| 	$: memoryIndex = parseIndex($page.url.searchParams.get('memory'), $memoryStore?.length - 1); | ||||
| 	$: assetIndex = parseIndex($page.url.searchParams.get('asset'), currentMemory?.assets.length - 1); | ||||
|  | ||||
| 	let assetIndex: number; | ||||
| 	$: { | ||||
| 		const index = parseInt($page.url.searchParams.get('asset') ?? '') || 0; | ||||
| 		assetIndex = index < currentMemory?.assets.length ? index : 0; | ||||
| 	} | ||||
| 	$: previousMemory = $memoryStore?.[memoryIndex - 1]; | ||||
| 	$: currentMemory = $memoryStore?.[memoryIndex]; | ||||
| 	$: nextMemory = $memoryStore?.[memoryIndex + 1]; | ||||
|  | ||||
| 	$: previousAsset = currentMemory?.assets[assetIndex - 1] || null; | ||||
| 	$: currentAsset = currentMemory?.assets[assetIndex] || null; | ||||
| 	$: nextAsset = currentMemory?.assets[assetIndex + 1] || null; | ||||
| 	$: previousAsset = currentMemory?.assets[assetIndex - 1]; | ||||
| 	$: currentAsset = currentMemory?.assets[assetIndex]; | ||||
| 	$: nextAsset = currentMemory?.assets[assetIndex + 1]; | ||||
|  | ||||
| 	$: canAdvance = !!(nextMemory || nextAsset); | ||||
|  | ||||
| 	$: if (!canAdvance && browser) { | ||||
| 		pause(); | ||||
| 	} | ||||
| 	const toNextMemory = () => goto(`?memory=${memoryIndex + 1}`); | ||||
| 	const toPreviousMemory = () => goto(`?memory=${memoryIndex - 1}`); | ||||
|  | ||||
| 	let memoryGallery: HTMLElement; | ||||
| 	let memoryWrapper: HTMLElement; | ||||
| 	let galleryInView = false; | ||||
| 	const toNextAsset = () => goto(`?memory=${memoryIndex}&asset=${assetIndex + 1}`); | ||||
| 	const toPreviousAsset = () => goto(`?memory=${memoryIndex}&asset=${assetIndex - 1}`); | ||||
|  | ||||
| 	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 () => { | ||||
| 		if (!$memoryStore) { | ||||
| @@ -59,67 +80,9 @@ | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	onDestroy(() => browser && pause()); | ||||
|  | ||||
| 	const toPreviousMemory = () => previousMemory && goto(`?memory=${memoryIndex - 1}`); | ||||
|  | ||||
| 	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(); | ||||
| 	let memoryGallery: HTMLElement; | ||||
| 	let memoryWrapper: HTMLElement; | ||||
| 	let galleryInView = false; | ||||
| </script> | ||||
|  | ||||
| <section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}> | ||||
| @@ -136,12 +99,12 @@ | ||||
| 					<CircleIconButton | ||||
| 						logo={paused ? Play : Pause} | ||||
| 						forceDark | ||||
| 						on:click={paused ? play : pause} | ||||
| 						on:click={() => (paused = !paused)} | ||||
| 					/> | ||||
|  | ||||
| 					<div class="relative w-full"> | ||||
| 						<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> | ||||
| @@ -215,7 +178,7 @@ | ||||
| 										<CircleIconButton | ||||
| 											logo={ChevronLeft} | ||||
| 											backgroundColor="#202123" | ||||
| 											on:click={toPreviousAsset} | ||||
| 											on:click={toPrevious} | ||||
| 										/> | ||||
| 									{/if} | ||||
| 								</div> | ||||
| @@ -226,7 +189,7 @@ | ||||
| 										<CircleIconButton | ||||
| 											logo={ChevronRight} | ||||
| 											backgroundColor="#202123" | ||||
| 											on:click={toNextAsset} | ||||
| 											on:click={toNext} | ||||
| 										/> | ||||
| 									{/if} | ||||
| 								</div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user