feat(nds): intro animation
This commit is contained in:
@@ -1,6 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useLoop, useTresContext } from "@tresjs/core";
|
import { useLoop, useTresContext } from "@tresjs/core";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
import gsap from "gsap";
|
||||||
|
|
||||||
|
const INTRO_ANIMATION = {
|
||||||
|
MODEL_SPIN_DURATION: 3,
|
||||||
|
LID_OPEN_DURATION: 2,
|
||||||
|
LID_OPEN_OVERLAP: 1.5,
|
||||||
|
|
||||||
|
CAMERA_START_POSITION: new THREE.Vector3(0, 15, 2.6),
|
||||||
|
CAMERA_START_ROTATION: new THREE.Euler(THREE.MathUtils.degToRad(-90), 0, 0),
|
||||||
|
CAMERA_END_POSITION: new THREE.Vector3(0, 14, 10),
|
||||||
|
CAMERA_END_ROTATION: new THREE.Euler(THREE.MathUtils.degToRad(-55), 0, 0),
|
||||||
|
CAMERA_DURATION: 3,
|
||||||
|
|
||||||
|
LID_CLOSED_ROTATION: THREE.MathUtils.degToRad(120),
|
||||||
|
LID_OPENED_ROTATION: THREE.MathUtils.degToRad(-30),
|
||||||
|
};
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
topScreenCanvas: HTMLCanvasElement | null;
|
topScreenCanvas: HTMLCanvasElement | null;
|
||||||
@@ -8,6 +24,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
const app = useAppStore();
|
||||||
|
|
||||||
const model = assets.nintendoDs.scene.clone(true);
|
const model = assets.nintendoDs.scene.clone(true);
|
||||||
|
|
||||||
@@ -18,6 +35,7 @@ let bottomScreenTexture: THREE.CanvasTexture | null = null;
|
|||||||
// screens
|
// screens
|
||||||
const TOP_SCREEN = "Object_9";
|
const TOP_SCREEN = "Object_9";
|
||||||
const BOTTOM_SCREEN = "Object_28";
|
const BOTTOM_SCREEN = "Object_28";
|
||||||
|
const LID = "Object_8";
|
||||||
|
|
||||||
// buttons
|
// buttons
|
||||||
const CROSS_BUTTON = "Object_21";
|
const CROSS_BUTTON = "Object_21";
|
||||||
@@ -41,11 +59,20 @@ const { camera, renderer } = useTresContext();
|
|||||||
|
|
||||||
model.scale.set(100, 100, 100);
|
model.scale.set(100, 100, 100);
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
watch(
|
watch(
|
||||||
() => camera.activeCamera.value,
|
() => camera.activeCamera.value,
|
||||||
(cam) => {
|
(cam) => {
|
||||||
if (cam) app.setCamera(cam);
|
if (cam) {
|
||||||
|
app.setCamera(cam);
|
||||||
|
|
||||||
|
if (app.booted) {
|
||||||
|
cam.position.copy(INTRO_ANIMATION.CAMERA_END_POSITION);
|
||||||
|
cam.rotation.copy(INTRO_ANIMATION.CAMERA_END_ROTATION);
|
||||||
|
} else {
|
||||||
|
cam.position.copy(INTRO_ANIMATION.CAMERA_START_POSITION);
|
||||||
|
cam.rotation.copy(INTRO_ANIMATION.CAMERA_START_ROTATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
@@ -57,6 +84,80 @@ model.traverse((child) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const lidMesh = requireMesh(LID);
|
||||||
|
const topScreenMesh = requireMesh(TOP_SCREEN);
|
||||||
|
|
||||||
|
if (app.booted) {
|
||||||
|
lidMesh.rotation.x = INTRO_ANIMATION.LID_OPENED_ROTATION;
|
||||||
|
topScreenMesh.rotation.x = INTRO_ANIMATION.LID_OPENED_ROTATION;
|
||||||
|
} else {
|
||||||
|
lidMesh.rotation.x = INTRO_ANIMATION.LID_CLOSED_ROTATION;
|
||||||
|
topScreenMesh.rotation.x = INTRO_ANIMATION.LID_CLOSED_ROTATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAnimated = ref(app.booted);
|
||||||
|
|
||||||
|
const animateIntro = () => {
|
||||||
|
if (hasAnimated.value) return;
|
||||||
|
hasAnimated.value = true;
|
||||||
|
|
||||||
|
const introTimeline = gsap.timeline();
|
||||||
|
|
||||||
|
introTimeline.to(
|
||||||
|
camera.activeCamera.value.position,
|
||||||
|
{
|
||||||
|
x: INTRO_ANIMATION.CAMERA_END_POSITION.x,
|
||||||
|
y: INTRO_ANIMATION.CAMERA_END_POSITION.y,
|
||||||
|
z: INTRO_ANIMATION.CAMERA_END_POSITION.z,
|
||||||
|
duration: INTRO_ANIMATION.CAMERA_DURATION,
|
||||||
|
ease: "power2.inOut",
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
introTimeline.to(
|
||||||
|
camera.activeCamera.value.rotation,
|
||||||
|
{
|
||||||
|
x: INTRO_ANIMATION.CAMERA_END_ROTATION.x,
|
||||||
|
y: INTRO_ANIMATION.CAMERA_END_ROTATION.y,
|
||||||
|
z: INTRO_ANIMATION.CAMERA_END_ROTATION.z,
|
||||||
|
duration: INTRO_ANIMATION.CAMERA_DURATION,
|
||||||
|
ease: "power2.inOut",
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
introTimeline.to(
|
||||||
|
model.rotation,
|
||||||
|
{
|
||||||
|
y: Math.PI * 2,
|
||||||
|
duration: INTRO_ANIMATION.MODEL_SPIN_DURATION,
|
||||||
|
ease: "power2.inOut",
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
introTimeline.to(
|
||||||
|
lidMesh.rotation,
|
||||||
|
{
|
||||||
|
x: INTRO_ANIMATION.LID_OPENED_ROTATION,
|
||||||
|
duration: INTRO_ANIMATION.LID_OPEN_DURATION,
|
||||||
|
ease: "power2.out",
|
||||||
|
},
|
||||||
|
INTRO_ANIMATION.MODEL_SPIN_DURATION - INTRO_ANIMATION.LID_OPEN_OVERLAP,
|
||||||
|
);
|
||||||
|
|
||||||
|
introTimeline.to(
|
||||||
|
topScreenMesh.rotation,
|
||||||
|
{
|
||||||
|
x: INTRO_ANIMATION.LID_OPENED_ROTATION,
|
||||||
|
duration: INTRO_ANIMATION.LID_OPEN_DURATION,
|
||||||
|
ease: "power2.out",
|
||||||
|
},
|
||||||
|
"<",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.topScreenCanvas, props.bottomScreenCanvas],
|
() => [props.topScreenCanvas, props.bottomScreenCanvas],
|
||||||
() => {
|
() => {
|
||||||
@@ -189,6 +290,11 @@ const pressButton = (button: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (event: MouseEvent) => {
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
if (!hasAnimated.value) {
|
||||||
|
animateIntro();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const domElement = renderer.instance.domElement;
|
const domElement = renderer.instance.domElement;
|
||||||
const rect = domElement.getBoundingClientRect();
|
const rect = domElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import * as THREE from "three";
|
|
||||||
import type { Screen as NDSScreen } from "#components";
|
import type { Screen as NDSScreen } from "#components";
|
||||||
|
|
||||||
const ENABLE_3D = true;
|
const ENABLE_3D = true;
|
||||||
@@ -56,11 +55,7 @@ useKeyUp((key) => {
|
|||||||
<LoadingScreen v-if="!isReady" />
|
<LoadingScreen v-if="!isReady" />
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<TresCanvas v-if="ENABLE_3D" window-size clear-color="#181818">
|
<TresCanvas v-if="ENABLE_3D" window-size clear-color="#181818">
|
||||||
<TresPerspectiveCamera
|
<TresPerspectiveCamera :args="[45, 1, 0.001, 1000]" />
|
||||||
:args="[45, 1, 0.001, 1000]"
|
|
||||||
:position="[0, 10, 12]"
|
|
||||||
:rotation="[THREE.MathUtils.degToRad(-38.3), 0, 0]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TresAmbientLight />
|
<TresAmbientLight />
|
||||||
<TresDirectionalLight />
|
<TresDirectionalLight />
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
import gsap from "gsap";
|
import gsap from "gsap";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
const ANIMATION = {
|
||||||
|
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: 3,
|
||||||
|
CAMERA_ROTATION_OVERLAP: 0.1,
|
||||||
|
FADE_DURATION: 1,
|
||||||
|
FADE_CAMERA_OVERLAP: 0.9,
|
||||||
|
NAVIGATE_DELAY: 3150,
|
||||||
|
};
|
||||||
|
|
||||||
export const useGalleryStore = defineStore("gallery", {
|
export const useGalleryStore = defineStore("gallery", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
intro: {
|
intro: {
|
||||||
@@ -18,93 +31,40 @@ export const useGalleryStore = defineStore("gallery", {
|
|||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
animateIntro() {
|
animateIntro() {
|
||||||
const CAMERA_DELAY = 0.7;
|
|
||||||
const CAMERA_DURATION = 3;
|
|
||||||
|
|
||||||
const FADE_DELAY = 0;
|
|
||||||
const FADE_DURATION = 1;
|
|
||||||
|
|
||||||
this.isIntro = true;
|
this.isIntro = true;
|
||||||
this.isOutro = false;
|
this.isOutro = false;
|
||||||
|
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
if (app.camera) {
|
|
||||||
gsap.fromTo(
|
|
||||||
app.camera.position,
|
|
||||||
{
|
|
||||||
x: 0,
|
|
||||||
y: 10,
|
|
||||||
z: 12,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: 0,
|
|
||||||
y: 3,
|
|
||||||
z: -1.7,
|
|
||||||
duration: CAMERA_DURATION,
|
|
||||||
delay: CAMERA_DELAY,
|
|
||||||
ease: "power2.inOut",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
gsap.fromTo(
|
// Intro: Fade starts first (at 0), camera starts after with overlap
|
||||||
app.camera.rotation,
|
const cameraDelay =
|
||||||
{
|
ANIMATION.FADE_DURATION - ANIMATION.FADE_CAMERA_OVERLAP;
|
||||||
x: THREE.MathUtils.degToRad(-38.3),
|
|
||||||
y: 0,
|
|
||||||
z: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: THREE.MathUtils.degToRad(-25),
|
|
||||||
y: 0,
|
|
||||||
z: 0,
|
|
||||||
duration: CAMERA_DURATION * 0.9,
|
|
||||||
delay: CAMERA_DELAY,
|
|
||||||
ease: "power2.inOut",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
gsap.fromTo(
|
gsap.fromTo(
|
||||||
this.intro,
|
this.intro,
|
||||||
{ fadeOpacity: 0 },
|
{ fadeOpacity: 0 },
|
||||||
{
|
{
|
||||||
fadeOpacity: 1,
|
fadeOpacity: 1,
|
||||||
duration: FADE_DURATION,
|
duration: ANIMATION.FADE_DURATION,
|
||||||
delay: FADE_DELAY,
|
delay: 0,
|
||||||
ease: "none",
|
ease: "none",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
navigateTo("/gallery");
|
|
||||||
}, 3150);
|
|
||||||
},
|
|
||||||
|
|
||||||
animateOutro() {
|
|
||||||
const CAMERA_DELAY = 0;
|
|
||||||
const CAMERA_DURATION = 3;
|
|
||||||
|
|
||||||
const FADE_DELAY = 2.3;
|
|
||||||
const FADE_DURATION = 1;
|
|
||||||
|
|
||||||
this.isIntro = false;
|
|
||||||
this.isOutro = true;
|
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
if (app.camera) {
|
if (app.camera) {
|
||||||
gsap.fromTo(
|
gsap.fromTo(
|
||||||
app.camera.position,
|
app.camera.position,
|
||||||
{
|
{
|
||||||
x: 0,
|
x: ANIMATION.NDS_CAMERA_POSITION.x,
|
||||||
y: 3,
|
y: ANIMATION.NDS_CAMERA_POSITION.y,
|
||||||
z: -1.7,
|
z: ANIMATION.NDS_CAMERA_POSITION.z,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
x: 0,
|
x: ANIMATION.GALLERY_CAMERA_POSITION.x,
|
||||||
y: 10,
|
y: ANIMATION.GALLERY_CAMERA_POSITION.y,
|
||||||
z: 12,
|
z: ANIMATION.GALLERY_CAMERA_POSITION.z,
|
||||||
duration: CAMERA_DURATION,
|
duration: ANIMATION.CAMERA_DURATION,
|
||||||
delay: CAMERA_DELAY,
|
delay: cameraDelay,
|
||||||
ease: "power2.inOut",
|
ease: "power2.inOut",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -112,16 +72,71 @@ export const useGalleryStore = defineStore("gallery", {
|
|||||||
gsap.fromTo(
|
gsap.fromTo(
|
||||||
app.camera.rotation,
|
app.camera.rotation,
|
||||||
{
|
{
|
||||||
x: THREE.MathUtils.degToRad(-25),
|
x: ANIMATION.NDS_CAMERA_ROTATION.x,
|
||||||
y: 0,
|
y: ANIMATION.NDS_CAMERA_ROTATION.y,
|
||||||
z: 0,
|
z: ANIMATION.NDS_CAMERA_ROTATION.z,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
x: THREE.MathUtils.degToRad(-38.3),
|
x: ANIMATION.GALLERY_CAMERA_ROTATION.x,
|
||||||
y: 0,
|
y: ANIMATION.GALLERY_CAMERA_ROTATION.y,
|
||||||
z: 0,
|
z: ANIMATION.GALLERY_CAMERA_ROTATION.z,
|
||||||
duration: CAMERA_DURATION * 0.9,
|
duration:
|
||||||
delay: CAMERA_DELAY,
|
ANIMATION.CAMERA_DURATION *
|
||||||
|
(1 - ANIMATION.CAMERA_ROTATION_OVERLAP),
|
||||||
|
delay: cameraDelay,
|
||||||
|
ease: "power2.inOut",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
navigateTo("/gallery");
|
||||||
|
}, ANIMATION.NAVIGATE_DELAY);
|
||||||
|
},
|
||||||
|
|
||||||
|
animateOutro() {
|
||||||
|
this.isIntro = false;
|
||||||
|
this.isOutro = true;
|
||||||
|
|
||||||
|
const app = useAppStore();
|
||||||
|
|
||||||
|
// Outro: Camera starts first (at 0), fade starts after with overlap
|
||||||
|
const fadeDelay =
|
||||||
|
ANIMATION.CAMERA_DURATION - 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,
|
||||||
|
delay: 0,
|
||||||
|
ease: "power2.inOut",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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 *
|
||||||
|
(1 - ANIMATION.CAMERA_ROTATION_OVERLAP),
|
||||||
|
delay: 0,
|
||||||
ease: "power2.inOut",
|
ease: "power2.inOut",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -132,8 +147,8 @@ export const useGalleryStore = defineStore("gallery", {
|
|||||||
{ fadeOpacity: 1 },
|
{ fadeOpacity: 1 },
|
||||||
{
|
{
|
||||||
fadeOpacity: 0,
|
fadeOpacity: 0,
|
||||||
duration: FADE_DURATION,
|
duration: ANIMATION.FADE_DURATION,
|
||||||
delay: FADE_DELAY,
|
delay: fadeDelay,
|
||||||
ease: "none",
|
ease: "none",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -141,7 +156,7 @@ export const useGalleryStore = defineStore("gallery", {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.isOutro = false;
|
this.isOutro = false;
|
||||||
app.navigateTo("home");
|
app.navigateTo("home");
|
||||||
}, 3310);
|
}, ANIMATION.NAVIGATE_DELAY);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user