1
0
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:
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"> <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 { onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { MemoryLaneResponseDto, api } from '@api'; import { api } from '@api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import Play from 'svelte-material-icons/Play.svelte'; import Play from 'svelte-material-icons/Play.svelte';
@ -19,15 +20,31 @@
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';
let currentIndex = 0; let memoryIndex: number;
let currentMemory: MemoryLaneResponseDto; $: {
let nextMemory: MemoryLaneResponseDto; const index = parseInt($page.url.searchParams.get('memory') ?? '') || 0;
let lastMemory: MemoryLaneResponseDto; memoryIndex = index < $memoryStore?.length ? index : 0;
}
let lastIndex = 0; $: previousMemory = $memoryStore?.[memoryIndex - 1] || null;
let nextIndex = 0; $: currentMemory = $memoryStore?.[memoryIndex] || null;
$: showNextMemory = nextIndex <= $memoryStore?.length - 1; $: nextMemory = $memoryStore?.[memoryIndex + 1] || null;
$: showPreviousMemory = currentIndex != 0;
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 memoryGallery: HTMLElement;
let memoryWrapper: HTMLElement; let memoryWrapper: HTMLElement;
@ -40,123 +57,69 @@
}); });
$memoryStore = data; $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 => { onDestroy(() => browser && pause());
if (showNextMemory) {
resetAutoPlay();
currentIndex++; const toPreviousMemory = () => previousMemory && goto(`?memory=${memoryIndex - 1}`);
nextIndex = currentIndex + 1;
lastIndex = currentIndex - 1;
currentMemory = $memoryStore[currentIndex]; const toNextMemory = () => nextMemory && goto(`?memory=${memoryIndex + 1}`);
nextMemory = $memoryStore[nextIndex];
lastMemory = $memoryStore[lastIndex];
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 = () => { const play = () => {
if (showPreviousMemory) { if (!canAdvance) {
resetAutoPlay(); return;
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;
}
} }
// Delay for nicer animation of the progress bar paused = false;
setTimeout(() => { requestDraw();
canPlayNext = true;
}, 250);
}; };
const resetAutoPlay = () => { const pause = () => {
autoPlayIndex = 0; paused = true;
autoPlayProgress = 0; cancelAnimationFrame(animationFrameRequest);
resetStart();
}; };
const toNextCurrentAsset = () => { const resetProgress = () => {
autoPlayIndex++; progress = 0;
resetStart();
if (autoPlayIndex > currentMemory.assets.length - 1) {
toNextMemory();
}
}; };
const toPreviousCurrentAsset = () => { const resetStart = () => (start = null);
autoPlayIndex--;
if (autoPlayIndex < 0) { // Progress should be reset when the current memory or asset changes.
toPreviousMemory(); $: 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}>
@ -170,19 +133,20 @@
{#if !galleryInView} {#if !galleryInView}
<div class="flex place-items-center place-content-center overflow-hidden gap-2"> <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"> <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 <span class="absolute left-0 h-[2px] bg-white" style:width={`${progress * 100}%`} />
class="absolute left-0 h-[2px] bg-white transition-all"
style:width={`${autoPlayProgress}%`}
/>
</div> </div>
<div> <div>
<p class="text-small"> <p class="text-small">
{autoPlayIndex + 1}/{currentMemory.assets.length} {assetIndex + 1}/{currentMemory.assets.length}
</p> </p>
</div> </div>
</div> </div>
@ -211,28 +175,28 @@
<!-- PREVIOUS MEMORY --> <!-- PREVIOUS MEMORY -->
<div <div
class="rounded-2xl w-[20vw] h-1/2" class="rounded-2xl w-[20vw] h-1/2"
class:opacity-25={showPreviousMemory} class:opacity-25={previousMemory}
class:opacity-0={!showPreviousMemory} class:opacity-0={!previousMemory}
class:hover:opacity-70={showPreviousMemory} class:hover:opacity-70={previousMemory}
> >
<button <button
class="rounded-2xl h-full w-full relative" class="rounded-2xl h-full w-full relative"
disabled={!showPreviousMemory} disabled={!previousMemory}
on:click={toPreviousMemory} on:click={toPreviousMemory}
> >
<img <img
class="rounded-2xl h-full w-full object-cover" class="rounded-2xl h-full w-full object-cover"
src={showPreviousMemory && lastMemory src={previousMemory
? api.getAssetThumbnailUrl(lastMemory.assets[0].id, 'JPEG') ? api.getAssetThumbnailUrl(previousMemory.assets[0].id, 'JPEG')
: noThumbnailUrl} : noThumbnailUrl}
alt="" alt=""
draggable="false" draggable="false"
/> />
{#if showPreviousMemory} {#if previousMemory}
<div class="absolute right-4 bottom-4 text-white text-left"> <div class="absolute right-4 bottom-4 text-white text-left">
<p class="font-semibold text-xs text-gray-200">PREVIOUS</p> <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> </div>
{/if} {/if}
</button> </button>
@ -247,29 +211,33 @@
<div class="absolute h-full flex justify-between w-full"> <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="flex h-full flex-col place-content-center place-items-center ml-4">
<div class="inline-block"> <div class="inline-block">
<CircleIconButton {#if previousMemory || previousAsset}
logo={ChevronLeft} <CircleIconButton
backgroundColor="#202123" logo={ChevronLeft}
on:click={toPreviousCurrentAsset} backgroundColor="#202123"
/> on:click={toPreviousAsset}
/>
{/if}
</div> </div>
</div> </div>
<div class="flex h-full flex-col place-content-center place-items-center mr-4"> <div class="flex h-full flex-col place-content-center place-items-center mr-4">
<div class="inline-block"> <div class="inline-block">
<CircleIconButton {#if canAdvance}
logo={ChevronRight} <CircleIconButton
backgroundColor="#202123" logo={ChevronRight}
on:click={toNextCurrentAsset} backgroundColor="#202123"
/> on:click={toNextAsset}
/>
{/if}
</div> </div>
</div> </div>
</div> </div>
{#key currentMemory.assets[autoPlayIndex].id} {#key currentAsset.id}
<img <img
transition:fade|local transition:fade|local
class="rounded-2xl w-full h-full object-contain transition-all" 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="" alt=""
draggable="false" draggable="false"
/> />
@ -282,8 +250,8 @@
)} )}
</p> </p>
<p> <p>
{currentMemory.assets[autoPlayIndex].exifInfo?.city || ''} {currentAsset.exifInfo?.city || ''}
{currentMemory.assets[autoPlayIndex].exifInfo?.country || ''} {currentAsset.exifInfo?.country || ''}
</p> </p>
</div> </div>
</div> </div>
@ -292,25 +260,25 @@
<!-- NEXT MEMORY --> <!-- NEXT MEMORY -->
<div <div
class="rounded-xl w-[20vw] h-1/2" class="rounded-xl w-[20vw] h-1/2"
class:opacity-25={showNextMemory} class:opacity-25={nextMemory}
class:opacity-0={!showNextMemory} class:opacity-0={!nextMemory}
class:hover:opacity-70={showNextMemory} class:hover:opacity-70={nextMemory}
> >
<button <button
class="rounded-2xl h-full w-full relative" class="rounded-2xl h-full w-full relative"
on:click={toNextMemory} on:click={toNextMemory}
disabled={!showNextMemory} disabled={!nextMemory}
> >
<img <img
class="rounded-2xl h-full w-full object-cover" class="rounded-2xl h-full w-full object-cover"
src={showNextMemory src={nextMemory
? api.getAssetThumbnailUrl(nextMemory.assets[0].id, 'JPEG') ? api.getAssetThumbnailUrl(nextMemory.assets[0].id, 'JPEG')
: noThumbnailUrl} : noThumbnailUrl}
alt="" alt=""
draggable="false" draggable="false"
/> />
{#if showNextMemory} {#if nextMemory}
<div class="absolute left-4 bottom-4 text-white text-left"> <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="font-semibold text-xs text-gray-200">UP NEXT</p>
<p class="text-xl">{nextMemory.title}</p> <p class="text-xl">{nextMemory.title}</p>

View File

@ -70,7 +70,7 @@
{#each memoryLane as memory, i (memory.title)} {#each memoryLane as memory, i (memory.title)}
<button <button
class="memory-card relative inline-block mr-8 rounded-xl aspect-video h-[215px]" 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 <img
class="rounded-xl h-full w-full object-cover" class="rounded-xl h-full w-full object-cover"