From 4aac82a97cd9016423f9bc2f6ca6d8c8f08b04ff Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Wed, 17 Dec 2025 20:06:46 +0100 Subject: [PATCH] feat(projects): intro and outro animations --- .../Projects/BottomScreen/BottomScreen.vue | 16 ++++-- .../Projects/BottomScreen/Buttons.vue | 14 +++--- app/components/Projects/TopScreen/Project.vue | 3 ++ .../Projects/TopScreen/TopScreen.vue | 7 +-- app/stores/projects.ts | 50 +++++++++++++++++++ 5 files changed, 77 insertions(+), 13 deletions(-) diff --git a/app/components/Projects/BottomScreen/BottomScreen.vue b/app/components/Projects/BottomScreen/BottomScreen.vue index b8ea5bb..d28f5cd 100644 --- a/app/components/Projects/BottomScreen/BottomScreen.vue +++ b/app/components/Projects/BottomScreen/BottomScreen.vue @@ -8,10 +8,20 @@ onMounted(async () => { store.$reset(); await store.loadProjects(); }); + +watch( + () => store.loading, + () => { + if (!store.loading) { + store.animateIntro(); + } + }, +); diff --git a/app/components/Projects/BottomScreen/Buttons.vue b/app/components/Projects/BottomScreen/Buttons.vue index fc8399e..5bd5e5e 100644 --- a/app/components/Projects/BottomScreen/Buttons.vue +++ b/app/components/Projects/BottomScreen/Buttons.vue @@ -90,7 +90,7 @@ useScreenClick((x, y) => { store.scrollProjects("right"); } else if (circleContains(BUTTONS.quit.position, [x, y], CLICK_RADIUS)) { startButtonAnimation("quit"); - throw new Error("quit"); + store.animateOutro(); } else if ( circleContains(BUTTONS.link.position, [x, y], CLICK_RADIUS) && project?.link @@ -102,9 +102,7 @@ useScreenClick((x, y) => { }); useRender((ctx) => { - if (!currentAnimation) return; - - if (currentAnimation.showButton) { + if (currentAnimation?.showButton) { const image = BUTTONS[currentAnimation.type].image; ctx.drawImage( image!, @@ -113,7 +111,7 @@ useRender((ctx) => { ); } - if (currentAnimation.showSmallCircle) { + if (currentAnimation?.showSmallCircle) { ctx.drawImage( assets.projects.bottomScreen.circleSmall, currentAnimation.position[0] - 28, @@ -121,15 +119,17 @@ useRender((ctx) => { ); } - if (currentAnimation.showBigCircle) { + if (currentAnimation?.showBigCircle) { ctx.drawImage( assets.projects.bottomScreen.circleBig, currentAnimation.position[0] - 44, currentAnimation.position[1] - 44, ); } -}); + ctx.fillStyle = `rgba(0, 0, 0, ${store.isIntro ? store.intro.fadeOpacity : store.isOutro ? store.outro.fadeOpacity : 0})`; + ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); +}); useKeyDown((key) => { if (currentAnimation) return; switch (key) { diff --git a/app/components/Projects/TopScreen/Project.vue b/app/components/Projects/TopScreen/Project.vue index c6ce896..6140a1f 100644 --- a/app/components/Projects/TopScreen/Project.vue +++ b/app/components/Projects/TopScreen/Project.vue @@ -124,6 +124,9 @@ useRender((ctx) => { "black", "black", ); + + ctx.fillStyle = `rgba(0, 0, 0, ${store.isIntro ? store.intro.fadeOpacity : store.isOutro ? store.outro.fadeOpacity : 0})`; + ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); }); defineOptions({ diff --git a/app/components/Projects/TopScreen/TopScreen.vue b/app/components/Projects/TopScreen/TopScreen.vue index 9f95050..1fd4e4f 100644 --- a/app/components/Projects/TopScreen/TopScreen.vue +++ b/app/components/Projects/TopScreen/TopScreen.vue @@ -6,7 +6,8 @@ const store = useProjectsStore(); diff --git a/app/stores/projects.ts b/app/stores/projects.ts index 2843842..442b84b 100644 --- a/app/stores/projects.ts +++ b/app/stores/projects.ts @@ -13,6 +13,17 @@ export const useProjectsStore = defineStore("projects", { currentProject: 0, loading: true, offsetX: 0, + + intro: { + fadeOpacity: 1, + }, + + outro: { + fadeOpacity: 0, + }, + + isIntro: true, + isOutro: false, }), actions: { @@ -97,5 +108,44 @@ export const useProjectsStore = defineStore("projects", { }, ); }, + + animateIntro() { + this.isIntro = true; + + gsap.fromTo( + this.intro, + { fadeOpacity: 1 }, + { + delay: 3, + fadeOpacity: 0, + duration: 0.35, + ease: "none", + onComplete: () => { + this.isIntro = false; + }, + }, + ); + }, + + animateOutro() { + this.isOutro = true; + + gsap.fromTo( + this.outro, + { fadeOpacity: 0 }, + { + delay: 0.5, + fadeOpacity: 1, + duration: 0.35, + ease: "none", + onComplete: () => { + setTimeout(() => { + const router = useRouter(); + router.push({ query: {} }); + }, 3000); + }, + }, + ); + }, }, });