feat(2d-nds): animation to and from gallery

This commit is contained in:
2026-02-23 17:25:29 +01:00
parent 89f0c9a2a5
commit 9f9cc90254
2 changed files with 58 additions and 17 deletions

View File

@@ -55,6 +55,17 @@ const ndsScale = computed(() => {
return Math.min(scaleX, scaleY); return Math.min(scaleX, scaleY);
}); });
const gallery = useGalleryStore();
const TOP_SCREEN_OFFSET = 170;
const zoomStyle = computed(() => {
const scale = ndsScale.value * gallery.zoom.scale;
if (scale === 1) return { scale: ndsScale.value };
const y = TOP_SCREEN_OFFSET * ndsScale.value * (gallery.zoom.scale - 1);
return { transform: `translateY(${y}px) scale(${scale})` };
});
watch( watch(
() => app.hintsVisible, () => app.hintsVisible,
async (show) => { async (show) => {
@@ -75,13 +86,11 @@ watch(
} }
}, },
); );
defineExpose({ ndsScale });
</script> </script>
<template> <template>
<div class="nds2d-container"> <div class="nds2d-container">
<div class="nds2d" :style="{ scale: ndsScale }"> <div class="nds2d" :style="zoomStyle">
<div class="nds2d-top-screen"> <div class="nds2d-top-screen">
<div class="nds2d-speaker-hole nds2d-sh1"></div> <div class="nds2d-speaker-hole nds2d-sh1"></div>
<div class="nds2d-speaker-hole nds2d-sh2"></div> <div class="nds2d-speaker-hole nds2d-sh2"></div>
@@ -204,6 +213,7 @@ defineExpose({ ndsScale });
align-items: center; align-items: center;
text-align: center; text-align: center;
background: #181818; background: #181818;
overflow: hidden;
} }
.nds2d { .nds2d {

View File

@@ -2,16 +2,22 @@ import gsap from "gsap";
import * as THREE from "three"; import * as THREE from "three";
const ANIMATION = { const ANIMATION = {
FADE_DURATION: 1,
FADE_CAMERA_OVERLAP: 0.9,
NAVIGATE_DELAY: 3150,
// 3D zoom
NDS_CAMERA_POSITION: new THREE.Vector3(0, 14, 10), NDS_CAMERA_POSITION: new THREE.Vector3(0, 14, 10),
NDS_CAMERA_ROTATION: new THREE.Euler(THREE.MathUtils.degToRad(-55), 0, 0), NDS_CAMERA_ROTATION: new THREE.Euler(THREE.MathUtils.degToRad(-55), 0, 0),
GALLERY_CAMERA_POSITION: new THREE.Vector3(0, 4.5, -3), GALLERY_CAMERA_POSITION: new THREE.Vector3(0, 4.5, -3),
GALLERY_CAMERA_ROTATION: new THREE.Euler(THREE.MathUtils.degToRad(-62), 0, 0), GALLERY_CAMERA_ROTATION: new THREE.Euler(THREE.MathUtils.degToRad(-62), 0, 0),
CAMERA_DURATION: 3, CAMERA_DURATION: 3,
CAMERA_ROTATION_OVERLAP: 0.1, CAMERA_ROTATION_OVERLAP: 0.1,
FADE_DURATION: 1,
FADE_CAMERA_OVERLAP: 0.9, // 2D zoom
NAVIGATE_DELAY: 3150, ZOOM_SCALE: 6,
ZOOM_DURATION: 3,
ZOOM_EASE: "power2.inOut",
}; };
export const useGalleryStore = defineStore("gallery", { export const useGalleryStore = defineStore("gallery", {
@@ -24,6 +30,10 @@ export const useGalleryStore = defineStore("gallery", {
fadeOpacity: 1, fadeOpacity: 1,
}, },
zoom: {
scale: 1,
},
isIntro: true, isIntro: true,
isOutro: false, isOutro: false,
shouldAnimateOutro: false, shouldAnimateOutro: false,
@@ -37,9 +47,8 @@ export const useGalleryStore = defineStore("gallery", {
const app = useAppStore(); const app = useAppStore();
app.disallowHints(); app.disallowHints();
// Intro: Fade starts first (at 0), camera starts after with overlap // Intro: Fade starts first (at 0), camera/zoom starts after with overlap
const cameraDelay = const zoomDelay = ANIMATION.FADE_DURATION - ANIMATION.FADE_CAMERA_OVERLAP;
ANIMATION.FADE_DURATION - ANIMATION.FADE_CAMERA_OVERLAP;
gsap.fromTo( gsap.fromTo(
this.intro, this.intro,
@@ -65,8 +74,8 @@ export const useGalleryStore = defineStore("gallery", {
y: ANIMATION.GALLERY_CAMERA_POSITION.y, y: ANIMATION.GALLERY_CAMERA_POSITION.y,
z: ANIMATION.GALLERY_CAMERA_POSITION.z, z: ANIMATION.GALLERY_CAMERA_POSITION.z,
duration: ANIMATION.CAMERA_DURATION, duration: ANIMATION.CAMERA_DURATION,
delay: cameraDelay, delay: zoomDelay,
ease: "power2.inOut", ease: ANIMATION.ZOOM_EASE,
}, },
); );
@@ -84,8 +93,19 @@ export const useGalleryStore = defineStore("gallery", {
duration: duration:
ANIMATION.CAMERA_DURATION * ANIMATION.CAMERA_DURATION *
(1 - ANIMATION.CAMERA_ROTATION_OVERLAP), (1 - ANIMATION.CAMERA_ROTATION_OVERLAP),
delay: cameraDelay, delay: zoomDelay,
ease: "power2.inOut", ease: ANIMATION.ZOOM_EASE,
},
);
} else {
gsap.fromTo(
this.zoom,
{ scale: 1 },
{
scale: ANIMATION.ZOOM_SCALE,
duration: ANIMATION.ZOOM_DURATION,
delay: zoomDelay,
ease: ANIMATION.ZOOM_EASE,
}, },
); );
} }
@@ -102,7 +122,7 @@ export const useGalleryStore = defineStore("gallery", {
const app = useAppStore(); const app = useAppStore();
// Outro: Camera starts first (at 0), fade starts after with overlap // Outro: Camera/zoom starts first (at 0), fade starts after with overlap
const fadeDelay = const fadeDelay =
ANIMATION.CAMERA_DURATION - ANIMATION.FADE_CAMERA_OVERLAP; ANIMATION.CAMERA_DURATION - ANIMATION.FADE_CAMERA_OVERLAP;
@@ -120,7 +140,7 @@ export const useGalleryStore = defineStore("gallery", {
z: ANIMATION.NDS_CAMERA_POSITION.z, z: ANIMATION.NDS_CAMERA_POSITION.z,
duration: ANIMATION.CAMERA_DURATION, duration: ANIMATION.CAMERA_DURATION,
delay: 0, delay: 0,
ease: "power2.inOut", ease: ANIMATION.ZOOM_EASE,
}, },
); );
@@ -139,7 +159,18 @@ export const useGalleryStore = defineStore("gallery", {
ANIMATION.CAMERA_DURATION * ANIMATION.CAMERA_DURATION *
(1 - ANIMATION.CAMERA_ROTATION_OVERLAP), (1 - ANIMATION.CAMERA_ROTATION_OVERLAP),
delay: 0, delay: 0,
ease: "power2.inOut", ease: ANIMATION.ZOOM_EASE,
},
);
} else {
gsap.fromTo(
this.zoom,
{ scale: ANIMATION.ZOOM_SCALE },
{
scale: 1,
duration: ANIMATION.ZOOM_DURATION,
delay: 0,
ease: ANIMATION.ZOOM_EASE,
}, },
); );
} }