import gsap from "gsap"; import * as THREE from "three"; 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_ROTATION: new THREE.Euler(THREE.MathUtils.degToRad(-55), 0, 0), GALLERY_CAMERA_POSITION: new THREE.Vector3(0, 4.5, -3), GALLERY_CAMERA_ROTATION: new THREE.Euler(THREE.MathUtils.degToRad(-62), 0, 0), CAMERA_DURATION_INTRO: 2.115, CAMERA_DURATION_OUTRO: 2.037, CAMERA_ROTATION_OVERLAP: 0.1, // 2D zoom ZOOM_SCALE: 6, ZOOM_DURATION_INTRO: 2.115, ZOOM_DURATION_OUTRO: 2.037, ZOOM_EASE: "power2.inOut", }; export const useGalleryStore = defineStore("gallery", { state: () => ({ intro: { fadeOpacity: 0, }, outro: { fadeOpacity: 1, }, zoom: { scale: 1, }, isIntro: true, isOutro: false, shouldAnimateOutro: false, }), actions: { cleanup() { gsap.killTweensOf(this.zoom); gsap.killTweensOf(this.intro); gsap.killTweensOf(this.outro); }, animateIntro() { this.isIntro = true; this.isOutro = false; const app = useAppStore(); app.disallowHints(); // Intro: Fade starts first (at 0), camera/zoom starts after with overlap const zoomDelay = ANIMATION.FADE_DURATION - ANIMATION.FADE_CAMERA_OVERLAP; gsap.fromTo( this.intro, { fadeOpacity: 0 }, { fadeOpacity: 1, duration: ANIMATION.FADE_DURATION, delay: 0, ease: "none", }, ); if (app.camera) { gsap.fromTo( app.camera.position, { x: ANIMATION.NDS_CAMERA_POSITION.x, y: ANIMATION.NDS_CAMERA_POSITION.y, z: ANIMATION.NDS_CAMERA_POSITION.z, }, { x: ANIMATION.GALLERY_CAMERA_POSITION.x, y: ANIMATION.GALLERY_CAMERA_POSITION.y, z: ANIMATION.GALLERY_CAMERA_POSITION.z, duration: ANIMATION.CAMERA_DURATION_INTRO, delay: zoomDelay, ease: ANIMATION.ZOOM_EASE, }, ); gsap.fromTo( app.camera.rotation, { x: ANIMATION.NDS_CAMERA_ROTATION.x, y: ANIMATION.NDS_CAMERA_ROTATION.y, z: ANIMATION.NDS_CAMERA_ROTATION.z, }, { x: ANIMATION.GALLERY_CAMERA_ROTATION.x, y: ANIMATION.GALLERY_CAMERA_ROTATION.y, z: ANIMATION.GALLERY_CAMERA_ROTATION.z, duration: ANIMATION.CAMERA_DURATION_INTRO * (1 - ANIMATION.CAMERA_ROTATION_OVERLAP), delay: zoomDelay, ease: ANIMATION.ZOOM_EASE, }, ); } else { gsap.fromTo( this.zoom, { scale: 1 }, { scale: ANIMATION.ZOOM_SCALE, duration: ANIMATION.ZOOM_DURATION_INTRO, delay: zoomDelay, ease: ANIMATION.ZOOM_EASE, }, ); } setTimeout(() => { this.shouldAnimateOutro = true; navigateTo("/gallery"); }, ANIMATION.NAVIGATE_DELAY); setTimeout(() => { const { assets } = useAssets(); assets.audio.whooshSmall.play(0.6); }, 100); }, animateOutro() { this.isIntro = false; this.isOutro = true; const app = useAppStore(); // Outro: Camera/zoom starts first (at 0), fade starts after with overlap const fadeDelay = ANIMATION.CAMERA_DURATION_OUTRO - ANIMATION.FADE_CAMERA_OVERLAP; if (app.camera) { gsap.fromTo( app.camera.position, { x: ANIMATION.GALLERY_CAMERA_POSITION.x, y: ANIMATION.GALLERY_CAMERA_POSITION.y, z: ANIMATION.GALLERY_CAMERA_POSITION.z, }, { x: ANIMATION.NDS_CAMERA_POSITION.x, y: ANIMATION.NDS_CAMERA_POSITION.y, z: ANIMATION.NDS_CAMERA_POSITION.z, duration: ANIMATION.CAMERA_DURATION_OUTRO, delay: 0, ease: ANIMATION.ZOOM_EASE, }, ); gsap.fromTo( app.camera.rotation, { x: ANIMATION.GALLERY_CAMERA_ROTATION.x, y: ANIMATION.GALLERY_CAMERA_ROTATION.y, z: ANIMATION.GALLERY_CAMERA_ROTATION.z, }, { x: ANIMATION.NDS_CAMERA_ROTATION.x, y: ANIMATION.NDS_CAMERA_ROTATION.y, z: ANIMATION.NDS_CAMERA_ROTATION.z, duration: ANIMATION.CAMERA_DURATION_OUTRO * (1 - ANIMATION.CAMERA_ROTATION_OVERLAP), delay: 0, ease: ANIMATION.ZOOM_EASE, }, ); } else { gsap.fromTo( this.zoom, { scale: ANIMATION.ZOOM_SCALE }, { scale: 1, duration: ANIMATION.ZOOM_DURATION_OUTRO, delay: 0, ease: ANIMATION.ZOOM_EASE, }, ); } gsap.fromTo( this.outro, { fadeOpacity: 1 }, { fadeOpacity: 0, duration: ANIMATION.FADE_DURATION, delay: fadeDelay, ease: "none", }, ); setTimeout(() => { this.isOutro = false; app.navigateTo("home"); app.allowHints(); }, ANIMATION.NAVIGATE_DELAY); const { assets } = useAssets(); assets.audio.whooshSmallReverse.play(0.6); }, }, });