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:
Thomas 2023-06-16 03:27:32 +01:00 committed by GitHub
parent 77fe2e55be
commit 43ffcf7e8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 112 additions and 144 deletions

View File

@ -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();
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;
}
return false;
toNextAsset();
start = now;
};
const toPreviousMemory = () => {
if (showPreviousMemory) {
resetAutoPlay();
currentIndex--;
nextIndex = currentIndex + 1;
lastIndex = currentIndex - 1;
currentMemory = $memoryStore[currentIndex];
nextMemory = $memoryStore[nextIndex];
lastMemory = $memoryStore[lastIndex];
}
};
let autoPlayInterval: NodeJS.Timeout;
let autoPlay = false;
let autoPlaySpeed = 5000;
let autoPlayProgress = 0;
let autoPlayIndex = 0;
let canPlayNext = true;
const toggleAutoPlay = () => {
autoPlay = !autoPlay;
if (autoPlay) {
autoPlayInterval = setInterval(() => {
if (!canPlayNext) return;
window.requestAnimationFrame(() => {
autoPlayProgress++;
});
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);
return;
}
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">
<CircleIconButton
logo={ChevronLeft}
backgroundColor="#202123"
on:click={toPreviousCurrentAsset}
/>
{#if previousMemory || previousAsset}
<CircleIconButton
logo={ChevronLeft}
backgroundColor="#202123"
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">
<CircleIconButton
logo={ChevronRight}
backgroundColor="#202123"
on:click={toNextCurrentAsset}
/>
{#if canAdvance}
<CircleIconButton
logo={ChevronRight}
backgroundColor="#202123"
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>

View File

@ -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"