feat(gallery): intro and outro animations

This commit is contained in:
2026-01-03 18:11:39 +01:00
parent bef5007f34
commit fe91377c3d

View File

@@ -52,41 +52,127 @@ const getAspectRatio = (image: InternalApi["/api/gallery"]["get"][number]) => {
};
const isAnimating = ref(true);
const router = useRouter();
onMounted(async () => {
const titleText = ref("");
const descriptionText = ref("");
const backButtonText = ref("");
const showBackButtonIcon = ref(false);
const ANIMATION_SLEEP = 0.25;
const TITLE_DURATION = 1.4;
const DESCRIPTION_DURATION = 1.6;
const BACK_BUTTON_DURATION = 0.6;
const FADE_IN_DELAY = 0.3;
const FADE_IN_DURATION = 1.5;
const FADE_IN_X_FACTOR = 0.15;
const FADE_IN_Y_FACTOR = 0.075;
const TITLE = "Pihkaal's Gallery";
const DESCRIPTION =
"Started on March 2025. I love taking photos of plants, insects, and arachnids.";
const BACK_BUTTON = "Back to Home";
const preventScroll = (e: Event) => e.preventDefault();
const typeText = (
target: Ref<string>,
text: string,
duration: number,
reverse = false,
cb?: { onStart?: () => void; onComplete?: () => void },
) =>
gsap.to(target, {
duration,
ease: `steps(${text.length})`,
onUpdate() {
const p = this.progress();
target.value = text.slice(
0,
reverse ? (1 - p) * text.length : p * text.length,
);
},
...cb,
});
const animateIntro = async () => {
await nextTick();
const scrollEl = scrollArea.value?.$el;
if (!scrollEl) return;
const preventScroll = (e: Event) => {
e.preventDefault();
e.stopPropagation();
};
scrollEl.addEventListener("wheel", preventScroll, { passive: false });
scrollEl.addEventListener("touchmove", preventScroll, { passive: false });
typeText(backButtonText, BACK_BUTTON, BACK_BUTTON_DURATION, false, {
onStart: () => (showBackButtonIcon.value = true),
}).delay(ANIMATION_SLEEP + FADE_IN_DELAY);
typeText(titleText, TITLE, TITLE_DURATION).delay(
ANIMATION_SLEEP + FADE_IN_DELAY,
);
typeText(descriptionText, DESCRIPTION, DESCRIPTION_DURATION).delay(
ANIMATION_SLEEP + FADE_IN_DELAY,
);
await gsap.fromTo(
".gallery-item",
{ opacity: 0 },
{
opacity: 1,
duration: FADE_IN_DURATION,
delay: ANIMATION_SLEEP,
stagger: (i) =>
(Math.floor(i / lanes.value) * FADE_IN_Y_FACTOR +
(i % lanes.value) * FADE_IN_X_FACTOR) *
FADE_IN_DURATION,
ease: "power2.in",
},
);
isAnimating.value = false;
scrollEl.removeEventListener("wheel", preventScroll);
scrollEl.removeEventListener("touchmove", preventScroll);
};
const animateOutro = async () => {
isAnimating.value = true;
const scrollEl = scrollArea.value?.$el;
if (!scrollEl) return;
scrollEl.addEventListener("wheel", preventScroll, { passive: false });
scrollEl.addEventListener("touchmove", preventScroll, { passive: false });
const items = document.querySelectorAll(".gallery-item");
gsap.fromTo(
items,
{ opacity: 0 },
{
opacity: 1,
duration: 0.6,
stagger: (index) => {
const line = Math.floor(index / lanes.value);
const column = index % lanes.value;
return line * 0.1 + column * 0.05;
},
gsap.to(".gallery-item", {
opacity: 0,
duration: FADE_IN_DURATION,
delay: FADE_IN_DELAY,
stagger: (i) =>
(Math.floor(i / lanes.value) * FADE_IN_Y_FACTOR +
(i % lanes.value) * FADE_IN_X_FACTOR) *
FADE_IN_DURATION,
ease: "power2.out",
onComplete: () => {
isAnimating.value = false;
});
typeText(backButtonText, BACK_BUTTON, BACK_BUTTON_DURATION, true, {
onComplete: () => (showBackButtonIcon.value = false),
});
typeText(titleText, TITLE, TITLE_DURATION, true);
typeText(descriptionText, DESCRIPTION, DESCRIPTION_DURATION, true, {
onComplete: async () => {
scrollEl.removeEventListener("wheel", preventScroll);
scrollEl.removeEventListener("touchmove", preventScroll);
await sleep(ANIMATION_SLEEP * 1000);
router.push("/");
},
},
);
});
};
onMounted(() => {
animateIntro();
});
</script>
@@ -109,18 +195,23 @@ onMounted(async () => {
size="md"
color="neutral"
variant="link"
to="/"
:ui="{
base: 'px-0 text-neutral-600 hover:text-neutral-400',
leadingIcon: 'size-4',
base: 'px-0 text-neutral-600 hover:text-neutral-400 h-8',
leadingIcon: showBackButtonIcon
? 'size-4'
: 'size-4 text-[#0a0a0a]',
}"
>Back to Home</UButton
@click="animateOutro"
>{{ backButtonText }}</UButton
>
<h1 class="text-3xl mt-3 font-bold">Pihkaal's Gallery</h1>
<p class="text-neutral-600 text-sm mt-2 w-4/5">
Started on March 2025. I love taking photos of plants, insects, and
arachnids.
<h1 class="text-3xl mt-3 font-bold relative">
<span class="invisible">{{ TITLE }}</span>
<span class="absolute inset-0">{{ titleText }}</span>
</h1>
<p class="text-neutral-600 text-sm mt-2 w-4/5 relative">
<span class="invisible">{{ DESCRIPTION }}</span>
<span class="absolute inset-0">{{ descriptionText }}</span>
</p>
</header>