mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
use animation frames for memory autoplay (#2771)
The current implementation mixes intervals and animation frames, which is a little convoluted. The use of intervals means that the animation is not going to be smooth and may have strange behaviour when the window is moved to the background. It's possible that the current animation frames could pile up and run all at once which would be undesirable. Moving everything into animation frames means the code is simpler and easier to reason about. It should also be more performant and less buggy. Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
77fe2e55be
commit
43ffcf7e8f
@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { memoryStore } from '$lib/stores/memory.store';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onMount } from 'svelte';
|
||||
import { MemoryLaneResponseDto, api } from '@api';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { api } from '@api';
|
||||
import { goto } from '$app/navigation';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import Play from 'svelte-material-icons/Play.svelte';
|
||||
@ -19,15 +20,31 @@
|
||||
import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
let currentIndex = 0;
|
||||
let currentMemory: MemoryLaneResponseDto;
|
||||
let nextMemory: MemoryLaneResponseDto;
|
||||
let lastMemory: MemoryLaneResponseDto;
|
||||
let memoryIndex: number;
|
||||
$: {
|
||||
const index = parseInt($page.url.searchParams.get('memory') ?? '') || 0;
|
||||
memoryIndex = index < $memoryStore?.length ? index : 0;
|
||||
}
|
||||
|
||||
let lastIndex = 0;
|
||||
let nextIndex = 0;
|
||||
$: showNextMemory = nextIndex <= $memoryStore?.length - 1;
|
||||
$: showPreviousMemory = currentIndex != 0;
|
||||
$: previousMemory = $memoryStore?.[memoryIndex - 1] || null;
|
||||
$: currentMemory = $memoryStore?.[memoryIndex] || null;
|
||||
$: nextMemory = $memoryStore?.[memoryIndex + 1] || null;
|
||||
|
||||
let assetIndex: number;
|
||||
$: {
|
||||
const index = parseInt($page.url.searchParams.get('asset') ?? '') || 0;
|
||||
assetIndex = index < currentMemory?.assets.length ? index : 0;
|
||||
}
|
||||
|
||||
$: previousAsset = currentMemory?.assets[assetIndex - 1] || null;
|
||||
$: currentAsset = currentMemory?.assets[assetIndex] || null;
|
||||
$: nextAsset = currentMemory?.assets[assetIndex + 1] || null;
|
||||
|
||||
$: canAdvance = !!(nextMemory || nextAsset);
|
||||
|
||||
$: if (!canAdvance && browser) {
|
||||
pause();
|
||||
}
|
||||
|
||||
let memoryGallery: HTMLElement;
|
||||
let memoryWrapper: HTMLElement;
|
||||
@ -40,123 +57,69 @@
|
||||
});
|
||||
$memoryStore = data;
|
||||
}
|
||||
|
||||
const queryIndex = $page.url.searchParams.get('index');
|
||||
if (queryIndex != null) {
|
||||
currentIndex = parseInt(queryIndex);
|
||||
if (isNaN(currentIndex) || currentIndex > $memoryStore.length - 1) {
|
||||
currentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
currentMemory = $memoryStore[currentIndex];
|
||||
|
||||
nextIndex = currentIndex + 1;
|
||||
nextMemory = $memoryStore[nextIndex];
|
||||
|
||||
if (currentIndex > 0) {
|
||||
lastMemory = $memoryStore[lastIndex];
|
||||
}
|
||||
});
|
||||
|
||||
const toNextMemory = (): boolean => {
|
||||
if (showNextMemory) {
|
||||
resetAutoPlay();
|
||||
onDestroy(() => browser && pause());
|
||||
|
||||
currentIndex++;
|
||||
nextIndex = currentIndex + 1;
|
||||
lastIndex = currentIndex - 1;
|
||||
const toPreviousMemory = () => previousMemory && goto(`?memory=${memoryIndex - 1}`);
|
||||
|
||||
currentMemory = $memoryStore[currentIndex];
|
||||
nextMemory = $memoryStore[nextIndex];
|
||||
lastMemory = $memoryStore[lastIndex];
|
||||
const toNextMemory = () => nextMemory && goto(`?memory=${memoryIndex + 1}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
const toPreviousAsset = () =>
|
||||
previousAsset ? goto(`?memory=${memoryIndex}&asset=${assetIndex - 1}`) : toPreviousMemory();
|
||||
|
||||
return false;
|
||||
};
|
||||
const toNextAsset = () =>
|
||||
nextAsset ? goto(`?memory=${memoryIndex}&asset=${assetIndex + 1}`) : toNextMemory();
|
||||
|
||||
const toPreviousMemory = () => {
|
||||
if (showPreviousMemory) {
|
||||
resetAutoPlay();
|
||||
const duration = 5000; // 5 seconds
|
||||
|
||||
currentIndex--;
|
||||
nextIndex = currentIndex + 1;
|
||||
lastIndex = currentIndex - 1;
|
||||
let paused = true;
|
||||
let progress = 0;
|
||||
let animationFrameRequest: number;
|
||||
let start: number | null = null;
|
||||
|
||||
currentMemory = $memoryStore[currentIndex];
|
||||
nextMemory = $memoryStore[nextIndex];
|
||||
lastMemory = $memoryStore[lastIndex];
|
||||
}
|
||||
};
|
||||
const requestDraw = () => (animationFrameRequest = requestAnimationFrame(draw));
|
||||
|
||||
let autoPlayInterval: NodeJS.Timeout;
|
||||
let autoPlay = false;
|
||||
let autoPlaySpeed = 5000;
|
||||
let autoPlayProgress = 0;
|
||||
let autoPlayIndex = 0;
|
||||
let canPlayNext = true;
|
||||
const draw = (now: number) => {
|
||||
requestDraw();
|
||||
|
||||
const toggleAutoPlay = () => {
|
||||
autoPlay = !autoPlay;
|
||||
if (autoPlay) {
|
||||
autoPlayInterval = setInterval(() => {
|
||||
if (!canPlayNext) return;
|
||||
start ??= now - progress * duration;
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
autoPlayProgress++;
|
||||
});
|
||||
const elapsed = now - start;
|
||||
progress = Math.min(1, elapsed / duration);
|
||||
|
||||
if (autoPlayProgress > 100) {
|
||||
autoPlayProgress = 0;
|
||||
canPlayNext = false;
|
||||
autoPlayTransition();
|
||||
}
|
||||
}, autoPlaySpeed / 100);
|
||||
} else {
|
||||
clearInterval(autoPlayInterval);
|
||||
}
|
||||
};
|
||||
|
||||
const autoPlayTransition = () => {
|
||||
if (autoPlayIndex < currentMemory.assets.length - 1) {
|
||||
autoPlayIndex++;
|
||||
} else {
|
||||
const canAdvance = toNextMemory();
|
||||
if (!canAdvance) {
|
||||
autoPlay = false;
|
||||
clearInterval(autoPlayInterval);
|
||||
if (progress !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
toNextAsset();
|
||||
start = now;
|
||||
};
|
||||
|
||||
const play = () => {
|
||||
if (!canAdvance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay for nicer animation of the progress bar
|
||||
setTimeout(() => {
|
||||
canPlayNext = true;
|
||||
}, 250);
|
||||
paused = false;
|
||||
requestDraw();
|
||||
};
|
||||
|
||||
const resetAutoPlay = () => {
|
||||
autoPlayIndex = 0;
|
||||
autoPlayProgress = 0;
|
||||
const pause = () => {
|
||||
paused = true;
|
||||
cancelAnimationFrame(animationFrameRequest);
|
||||
resetStart();
|
||||
};
|
||||
|
||||
const toNextCurrentAsset = () => {
|
||||
autoPlayIndex++;
|
||||
|
||||
if (autoPlayIndex > currentMemory.assets.length - 1) {
|
||||
toNextMemory();
|
||||
}
|
||||
const resetProgress = () => {
|
||||
progress = 0;
|
||||
resetStart();
|
||||
};
|
||||
|
||||
const toPreviousCurrentAsset = () => {
|
||||
autoPlayIndex--;
|
||||
const resetStart = () => (start = null);
|
||||
|
||||
if (autoPlayIndex < 0) {
|
||||
toPreviousMemory();
|
||||
}
|
||||
};
|
||||
// Progress should be reset when the current memory or asset changes.
|
||||
$: memoryIndex, assetIndex, resetProgress();
|
||||
</script>
|
||||
|
||||
<section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}>
|
||||
@ -170,19 +133,20 @@
|
||||
|
||||
{#if !galleryInView}
|
||||
<div class="flex place-items-center place-content-center overflow-hidden gap-2">
|
||||
<CircleIconButton logo={autoPlay ? Pause : Play} forceDark on:click={toggleAutoPlay} />
|
||||
<CircleIconButton
|
||||
logo={paused ? Play : Pause}
|
||||
forceDark
|
||||
on:click={paused ? play : pause}
|
||||
/>
|
||||
|
||||
<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 transition-all"
|
||||
style:width={`${autoPlayProgress}%`}
|
||||
/>
|
||||
<span class="absolute left-0 h-[2px] bg-white" style:width={`${progress * 100}%`} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-small">
|
||||
{autoPlayIndex + 1}/{currentMemory.assets.length}
|
||||
{assetIndex + 1}/{currentMemory.assets.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -211,28 +175,28 @@
|
||||
<!-- PREVIOUS MEMORY -->
|
||||
<div
|
||||
class="rounded-2xl w-[20vw] h-1/2"
|
||||
class:opacity-25={showPreviousMemory}
|
||||
class:opacity-0={!showPreviousMemory}
|
||||
class:hover:opacity-70={showPreviousMemory}
|
||||
class:opacity-25={previousMemory}
|
||||
class:opacity-0={!previousMemory}
|
||||
class:hover:opacity-70={previousMemory}
|
||||
>
|
||||
<button
|
||||
class="rounded-2xl h-full w-full relative"
|
||||
disabled={!showPreviousMemory}
|
||||
disabled={!previousMemory}
|
||||
on:click={toPreviousMemory}
|
||||
>
|
||||
<img
|
||||
class="rounded-2xl h-full w-full object-cover"
|
||||
src={showPreviousMemory && lastMemory
|
||||
? api.getAssetThumbnailUrl(lastMemory.assets[0].id, 'JPEG')
|
||||
src={previousMemory
|
||||
? api.getAssetThumbnailUrl(previousMemory.assets[0].id, 'JPEG')
|
||||
: noThumbnailUrl}
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
|
||||
{#if showPreviousMemory}
|
||||
{#if previousMemory}
|
||||
<div class="absolute right-4 bottom-4 text-white text-left">
|
||||
<p class="font-semibold text-xs text-gray-200">PREVIOUS</p>
|
||||
<p class="text-xl">{lastMemory.title}</p>
|
||||
<p class="text-xl">{previousMemory.title}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
@ -247,29 +211,33 @@
|
||||
<div class="absolute h-full flex justify-between w-full">
|
||||
<div class="flex h-full flex-col place-content-center place-items-center ml-4">
|
||||
<div class="inline-block">
|
||||
{#if previousMemory || previousAsset}
|
||||
<CircleIconButton
|
||||
logo={ChevronLeft}
|
||||
backgroundColor="#202123"
|
||||
on:click={toPreviousCurrentAsset}
|
||||
on:click={toPreviousAsset}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex h-full flex-col place-content-center place-items-center mr-4">
|
||||
<div class="inline-block">
|
||||
{#if canAdvance}
|
||||
<CircleIconButton
|
||||
logo={ChevronRight}
|
||||
backgroundColor="#202123"
|
||||
on:click={toNextCurrentAsset}
|
||||
on:click={toNextAsset}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#key currentMemory.assets[autoPlayIndex].id}
|
||||
{#key currentAsset.id}
|
||||
<img
|
||||
transition:fade|local
|
||||
class="rounded-2xl w-full h-full object-contain transition-all"
|
||||
src={api.getAssetThumbnailUrl(currentMemory.assets[autoPlayIndex].id, 'JPEG')}
|
||||
src={api.getAssetThumbnailUrl(currentAsset.id, 'JPEG')}
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
@ -282,8 +250,8 @@
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{currentMemory.assets[autoPlayIndex].exifInfo?.city || ''}
|
||||
{currentMemory.assets[autoPlayIndex].exifInfo?.country || ''}
|
||||
{currentAsset.exifInfo?.city || ''}
|
||||
{currentAsset.exifInfo?.country || ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -292,25 +260,25 @@
|
||||
<!-- NEXT MEMORY -->
|
||||
<div
|
||||
class="rounded-xl w-[20vw] h-1/2"
|
||||
class:opacity-25={showNextMemory}
|
||||
class:opacity-0={!showNextMemory}
|
||||
class:hover:opacity-70={showNextMemory}
|
||||
class:opacity-25={nextMemory}
|
||||
class:opacity-0={!nextMemory}
|
||||
class:hover:opacity-70={nextMemory}
|
||||
>
|
||||
<button
|
||||
class="rounded-2xl h-full w-full relative"
|
||||
on:click={toNextMemory}
|
||||
disabled={!showNextMemory}
|
||||
disabled={!nextMemory}
|
||||
>
|
||||
<img
|
||||
class="rounded-2xl h-full w-full object-cover"
|
||||
src={showNextMemory
|
||||
src={nextMemory
|
||||
? api.getAssetThumbnailUrl(nextMemory.assets[0].id, 'JPEG')
|
||||
: noThumbnailUrl}
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
|
||||
{#if showNextMemory}
|
||||
{#if nextMemory}
|
||||
<div class="absolute left-4 bottom-4 text-white text-left">
|
||||
<p class="font-semibold text-xs text-gray-200">UP NEXT</p>
|
||||
<p class="text-xl">{nextMemory.title}</p>
|
||||
|
@ -70,7 +70,7 @@
|
||||
{#each memoryLane as memory, i (memory.title)}
|
||||
<button
|
||||
class="memory-card relative inline-block mr-8 rounded-xl aspect-video h-[215px]"
|
||||
on:click={() => goto(`/memory?index=${i}`)}
|
||||
on:click={() => goto(`/memory?memory=${i}`)}
|
||||
>
|
||||
<img
|
||||
class="rounded-xl h-full w-full object-cover"
|
||||
|
Loading…
Reference in New Issue
Block a user