Skip to content

Commit 8698318

Browse files
authored
Improve animation when switching images (#3354)
1 parent 19ee56e commit 8698318

4 files changed

Lines changed: 165 additions & 106 deletions

File tree

resources/js/components/gallery/photoModule/PhotoPanel.vue

Lines changed: 140 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,103 @@
11
<template>
2-
<div
3-
id="shutter"
4-
:class="{
5-
'absolute w-screen h-dvh bg-surface-950 transition-opacity duration-1000 ease-in-out top-0 left-0': true,
6-
'z-50 opacity-0 pointer-events-none': is_slideshow_active,
7-
}"
8-
></div>
92
<div class="absolute z-20 top-0 left-0 w-full flex h-full overflow-hidden bg-black">
103
<PhotoHeader :photo="props.photo" @toggle-slide-show="emits('toggleSlideShow')" @go-back="emits('goBack')" />
114
<div class="w-0 flex-auto relative">
12-
<div
13-
id="imageview"
14-
class="absolute top-0 left-0 w-full h-full bg-black flex items-center justify-center overflow-hidden"
15-
@click="emits('rotateOverlay')"
16-
ref="swipe"
17-
:class="{
18-
'pt-14': imageViewMode === ImageViewMode.Pdf && !is_full_screen,
19-
}"
20-
>
21-
<!-- This is a video file: put html5 player -->
22-
<video
23-
v-if="imageViewMode == ImageViewMode.Video"
24-
width="auto"
25-
height="auto"
26-
id="image"
27-
ref="videoElement"
28-
controls
29-
class="absolute m-auto w-auto h-auto"
30-
:class="is_full_screen || is_slideshow_active ? 'max-w-full max-h-full' : 'max-wh-full-56'"
31-
autobuffer
32-
:autoplay="lycheeStore.can_autoplay"
33-
>
34-
<source :src="props.photo.size_variants.original?.url ?? ''" />
35-
Your browser does not support the video tag.
36-
</video>
37-
<!-- This is a raw file: put a place holder -->
38-
<embed
39-
v-if="imageViewMode == ImageViewMode.Pdf"
40-
id="image"
41-
alt="pdf"
42-
:src="props.photo.size_variants.original?.url ?? ''"
43-
type="application/pdf"
44-
frameBorder="0"
45-
scrolling="auto"
46-
class="absolute m-auto animate-zoomIn bg-contain bg-center bg-no-repeat"
47-
height="90%"
48-
width="100%"
49-
/>
50-
<!-- This is a raw file: put a place holder -->
51-
<img
52-
v-if="imageViewMode == ImageViewMode.Raw"
53-
id="image"
54-
alt="placeholder"
55-
class="absolute m-auto w-auto h-auto animate-zoomIn bg-contain bg-center bg-no-repeat"
56-
:src="getPlaceholderIcon()"
57-
/>
58-
<!-- This is a normal image: medium or original -->
59-
<img
60-
v-if="imageViewMode == ImageViewMode.Medium"
61-
id="image"
62-
alt="medium"
63-
class="absolute m-auto w-auto h-auto animate-zoomIn bg-contain bg-center bg-no-repeat"
64-
:src="props.photo.size_variants.medium?.url ?? ''"
65-
:class="is_full_screen || is_slideshow_active ? 'max-w-full max-h-full' : 'max-wh-full-56'"
66-
:srcset="srcSetMedium"
67-
/>
68-
<img
69-
v-if="imageViewMode == ImageViewMode.Original"
70-
id="image"
71-
alt="big"
72-
class="absolute m-auto w-auto h-auto animate-zoomIn bg-contain bg-center bg-no-repeat"
73-
:class="is_full_screen || is_slideshow_active ? 'max-w-full max-h-full' : 'max-wh-full-56'"
74-
:style="style"
75-
:src="props.photo.size_variants.original?.url ?? ''"
76-
/>
77-
<!-- This is a livephoto : medium -->
78-
<div
79-
v-if="imageViewMode == ImageViewMode.LivePhotoMedium"
80-
id="livephoto"
81-
data-live-photo
82-
data-proactively-loads-video="true"
83-
:data-photo-src="photo?.size_variants.medium?.url"
84-
:data-video-src="photo?.live_photo_url"
85-
class="absolute m-auto w-auto h-auto"
86-
:class="is_full_screen || is_slideshow_active ? 'max-w-full max-h-full' : 'max-wh-full-56'"
87-
:style="style"
88-
></div>
89-
<!-- This is a livephoto : full -->
90-
<div
91-
v-if="imageViewMode == ImageViewMode.LivePhotoOriginal"
92-
id="livephoto"
93-
data-live-photo
94-
data-proactively-loads-video="true"
95-
:data-photo-src="photo?.size_variants.original?.url"
96-
:data-video-src="photo?.live_photo_url"
97-
class="absolute m-auto w-auto h-auto"
98-
:class="is_full_screen || is_slideshow_active ? 'max-w-full max-h-full' : 'max-wh-full-56'"
99-
:style="style"
100-
></div>
101-
102-
<!-- <x-gallery.photo.overlay /> -->
5+
<div class="animate-zoomIn w-full h-full">
6+
<Transition :name="props.transition">
7+
<div
8+
:key="photo.id"
9+
id="imageview"
10+
class="absolute top-0 left-0 w-full h-full flex items-center justify-center overflow-hidden"
11+
@click="emits('rotateOverlay')"
12+
ref="swipe"
13+
:class="{
14+
'pt-14': imageViewMode === ImageViewMode.Pdf && !is_full_screen,
15+
}"
16+
>
17+
<!-- This is a video file: put html5 player -->
18+
<video
19+
v-if="imageViewMode == ImageViewMode.Video"
20+
width="auto"
21+
height="auto"
22+
id="image"
23+
ref="videoElement"
24+
controls
25+
class="absolute m-auto w-auto h-auto"
26+
:class="is_full_screen || is_slideshow_active ? 'max-w-full max-h-full' : 'max-wh-full-56'"
27+
autobuffer
28+
:autoplay="lycheeStore.can_autoplay"
29+
>
30+
<source :src="props.photo.size_variants.original?.url ?? ''" />
31+
Your browser does not support the video tag.
32+
</video>
33+
<!-- This is a raw file: put a place holder -->
34+
<embed
35+
v-if="imageViewMode == ImageViewMode.Pdf"
36+
id="image"
37+
alt="pdf"
38+
:src="props.photo.size_variants.original?.url ?? ''"
39+
type="application/pdf"
40+
frameBorder="0"
41+
scrolling="auto"
42+
class="absolute m-auto bg-contain bg-center bg-no-repeat"
43+
height="90%"
44+
width="100%"
45+
/>
46+
<!-- This is a raw file: put a place holder -->
47+
<img
48+
v-if="imageViewMode == ImageViewMode.Raw"
49+
id="image"
50+
alt="placeholder"
51+
class="absolute m-auto w-auto h-auto bg-contain bg-center bg-no-repeat"
52+
:src="getPlaceholderIcon()"
53+
/>
54+
<!-- This is a normal image: medium or original -->
55+
<img
56+
v-if="imageViewMode == ImageViewMode.Medium"
57+
id="image"
58+
alt="medium"
59+
class="absolute m-auto w-auto h-auto bg-contain bg-center bg-no-repeat"
60+
:src="props.photo.size_variants.medium?.url ?? ''"
61+
:class="is_full_screen || is_slideshow_active ? 'max-w-full max-h-full' : 'max-wh-full-56'"
62+
:srcset="srcSetMedium"
63+
/>
64+
<img
65+
v-if="imageViewMode == ImageViewMode.Original"
66+
id="image"
67+
alt="big"
68+
class="absolute m-auto w-auto h-auto bg-contain bg-center bg-no-repeat"
69+
:class="is_full_screen || is_slideshow_active ? 'max-w-full max-h-full' : 'max-wh-full-56'"
70+
:style="style"
71+
:src="props.photo.size_variants.original?.url ?? ''"
72+
/>
73+
<!-- This is a livephoto : medium -->
74+
<div
75+
v-if="imageViewMode == ImageViewMode.LivePhotoMedium"
76+
id="livephoto"
77+
data-live-photo
78+
data-proactively-loads-video="true"
79+
:data-photo-src="photo?.size_variants.medium?.url"
80+
:data-video-src="photo?.live_photo_url"
81+
class="absolute m-auto w-auto h-auto"
82+
:class="is_full_screen || is_slideshow_active ? 'max-w-full max-h-full' : 'max-wh-full-56'"
83+
:style="style"
84+
></div>
85+
<!-- This is a livephoto : full -->
86+
<div
87+
v-if="imageViewMode == ImageViewMode.LivePhotoOriginal"
88+
id="livephoto"
89+
data-live-photo
90+
data-proactively-loads-video="true"
91+
:data-photo-src="photo?.size_variants.original?.url"
92+
:data-video-src="photo?.live_photo_url"
93+
class="absolute m-auto w-auto h-auto"
94+
:class="is_full_screen || is_slideshow_active ? 'max-w-full max-h-full' : 'max-wh-full-56'"
95+
:style="style"
96+
></div>
97+
98+
<!-- <x-gallery.photo.overlay /> -->
99+
</div>
100+
</Transition>
103101
</div>
104102
<NextPrevious
105103
v-if="photo.previous_photo_id !== null && !is_slideshow_active"
@@ -148,6 +146,7 @@ import { useSwipe, type UseSwipeDirection } from "@vueuse/core";
148146
import { shouldIgnoreKeystroke } from "@/utils/keybindings-utils";
149147
import { onUnmounted } from "vue";
150148
import { useDebounceFn } from "@vueuse/core";
149+
import { Transition } from "vue";
151150
152151
const swipe = ref<HTMLElement | null>(null);
153152
const videoElement = ref<HTMLVideoElement | null>(null);
@@ -163,6 +162,7 @@ const props = defineProps<{
163162
photo: App.Http.Resources.Models.PhotoResource;
164163
photos: App.Http.Resources.Models.PhotoResource[];
165164
isMapVisible: boolean;
165+
transition: "slide-next" | "slide-previous";
166166
}>();
167167
168168
const photo = ref(props.photo);
@@ -235,3 +235,45 @@ watch(
235235
},
236236
);
237237
</script>
238+
239+
<style lang="css">
240+
.slide-next-leave-active,
241+
.slide-next-enter-active {
242+
transition:
243+
transform 0.5s cubic-bezier(0.165, 0.84, 0.44, 1),
244+
opacity 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95);
245+
}
246+
.slide-next-enter-from {
247+
transform: translate(5%, 0);
248+
opacity: 0;
249+
}
250+
.slide-next-enter-to,
251+
.slide-next-leave-from {
252+
transform: translate(0, 0);
253+
opacity: 1;
254+
}
255+
.slide-next-leave-to {
256+
transform: translate(-5%, 0);
257+
opacity: 0;
258+
}
259+
260+
.slide-previous-leave-active,
261+
.slide-previous-enter-active {
262+
transition:
263+
transform 0.5s cubic-bezier(0.165, 0.84, 0.44, 1),
264+
opacity 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95);
265+
}
266+
.slide-previous-enter-from {
267+
transform: translate(-5%, 0);
268+
opacity: 0;
269+
}
270+
.slide-previous-enter-to,
271+
.slide-previous-leave-from {
272+
transform: translate(0, 0);
273+
opacity: 1;
274+
}
275+
.slide-previous-leave-to {
276+
transform: translate(5%, 0);
277+
opacity: 0;
278+
}
279+
</style>

resources/js/composables/album/albumRefresher.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export function useAlbumRefresher(albumId: Ref<string>, photoId: Ref<string | un
1616
const smartAlbum = ref<App.Http.Resources.Models.SmartAlbumResource | undefined>(undefined);
1717
const album = computed(() => modelAlbum.value || tagAlbum.value || smartAlbum.value);
1818

19+
const transition = ref<"slide-next" | "slide-previous">("slide-next");
1920
const photo = ref<App.Http.Resources.Models.PhotoResource | undefined>(undefined);
2021
const photos = ref<App.Http.Resources.Models.PhotoResource[]>([]);
2122
const photosTimeline = ref<SplitData<App.Http.Resources.Models.PhotoResource>[] | undefined>(undefined);
@@ -105,6 +106,18 @@ export function useAlbumRefresher(albumId: Ref<string>, photoId: Ref<string | un
105106
});
106107
}
107108

109+
function setTransition(photo_id: string | undefined | null) {
110+
if (photo_id === undefined || photo_id === null) {
111+
return;
112+
}
113+
114+
if (photo.value !== undefined) {
115+
transition.value = photo.value.next_photo_id === photo_id ? "slide-next" : "slide-previous";
116+
} else {
117+
transition.value = "slide-next";
118+
}
119+
}
120+
108121
return {
109122
isPasswordProtected,
110123
isLoading,
@@ -115,12 +128,14 @@ export function useAlbumRefresher(albumId: Ref<string>, photoId: Ref<string | un
115128
smartAlbum,
116129
album,
117130
rights,
131+
transition,
118132
photo,
119133
photos,
120134
photosTimeline,
121135
config,
122136
loadUser,
123137
loadAlbum,
124138
refresh,
139+
setTransition,
125140
};
126141
}

resources/js/views/gallery-panels/Album.vue

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
:photo="photo"
3838
:photos="photos"
3939
:is-map-visible="config?.is_map_accessible ?? false"
40+
:transition="transition"
4041
@toggle-slide-show="slideshow"
4142
@rotate-overlay="rotateOverlay"
4243
@rotate-photo-c-w="rotatePhotoCW"
@@ -50,7 +51,6 @@
5051
@next="() => next(true)"
5152
@previous="() => previous(true)"
5253
/>
53-
5454
<!-- Dialogs -->
5555
<template v-if="photo">
5656
<PhotoEdit v-if="photo?.rights.can_edit" :photo="photo" v-model:visible="is_photo_edit_open" />
@@ -217,12 +217,9 @@ const { is_delete_visible, toggleDelete, is_merge_album_visible, is_move_visible
217217
useGalleryModals(togglableStore);
218218
219219
// Set up Album ID reference. This one is updated at each page change.
220-
const { isPasswordProtected, isLoading, user, modelAlbum, album, photo, photosTimeline, rights, photos, config, refresh } = useAlbumRefresher(
221-
albumId,
222-
photoId,
223-
auth,
224-
is_login_open,
225-
);
220+
const { isPasswordProtected, isLoading, user, modelAlbum, album, photo, transition, photosTimeline, rights, photos, config, refresh, setTransition } =
221+
useAlbumRefresher(albumId, photoId, auth, is_login_open);
222+
226223
const { refreshPhoto } = usePhotoRefresher(photo, photos, photoId);
227224
228225
const children = computed<App.Http.Resources.Models.ThumbAlbumResource[]>(() => modelAlbum.value?.albums ?? []);
@@ -431,6 +428,8 @@ watch(
431428
([newAlbumId, newPhotoId], _) => {
432429
unselect();
433430
431+
setTransition(newPhotoId as string | undefined);
432+
434433
albumId.value = newAlbumId as string;
435434
photoId.value = newPhotoId as string;
436435
debouncedPhotoMetrics();

resources/js/views/gallery-panels/Search.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
:photo="photo"
5151
:photos="photosForSelection"
5252
:is-map-visible="config?.is_map_accessible ?? false"
53+
:transition="transition"
5354
@toggle-slide-show="slideshow"
5455
@rotate-overlay="rotateOverlay"
5556
@rotate-photo-c-w="rotatePhotoCW"
@@ -207,7 +208,7 @@ const search_term = ref("");
207208
208209
const { is_login_open, is_slideshow_active, is_photo_edit_open, is_full_screen, are_details_open } = storeToRefs(togglableStore);
209210
210-
const { album, photo, config, loadAlbum } = useAlbumRefresher(albumId, photoId, auth, is_login_open);
211+
const { album, photo, transition, config, loadAlbum, setTransition } = useAlbumRefresher(albumId, photoId, auth, is_login_open);
211212
212213
const {
213214
albums,
@@ -447,6 +448,8 @@ watch(
447448
(newPhotoId, _) => {
448449
unselect();
449450
451+
setTransition(newPhotoId as string | undefined);
452+
450453
photoId.value = newPhotoId as string;
451454
debouncedPhotoMetrics();
452455
if (photoId.value !== undefined) {

0 commit comments

Comments
 (0)