feat(gallery): transition to and from the nds

This commit is contained in:
2026-01-04 20:53:52 +01:00
parent 1f61eee93a
commit 18f91981ec
13 changed files with 253 additions and 14 deletions

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
const { onRender } = useScreen();
const store = useGalleryStore();
const { assets } = useAssets();
onRender((ctx) => {
ctx.drawImage(assets.home.bottomScreen.background, 0, 0);
ctx.fillStyle = "#000000";
ctx.globalAlpha = store.isIntro
? store.intro.fadeOpacity
: store.isOutro
? store.outro.fadeOpacity
: 0;
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
});
</script>
<template>
<div />
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
const { onRender } = useScreen();
const store = useGalleryStore();
const { assets } = useAssets();
onMounted(() => {
if (store.shouldAnimateOutro) {
store.shouldAnimateOutro = false;
store.animateOutro();
} else {
store.$reset();
store.animateIntro();
}
});
onRender((ctx) => {
ctx.drawImage(assets.home.topScreen.background, 0, 0);
ctx.fillStyle = "#000000";
ctx.globalAlpha = store.isIntro
? store.intro.fadeOpacity
: store.isOutro
? store.outro.fadeOpacity
: 0;
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
});
</script>
<template>
<div />
</template>

View File

@@ -11,7 +11,7 @@ const { selectedButton, selectorPosition } = useButtonNavigation({
buttons: { buttons: {
projects: [31, 23, 193, 49], projects: [31, 23, 193, 49],
contact: [31, 71, 97, 49], contact: [31, 71, 97, 49],
downloadPlay: [127, 71, 97, 49], gallery: [127, 71, 97, 49],
theme: [0, 167, 31, 26], theme: [0, 167, 31, 26],
settings: [112, 167, 31, 26], settings: [112, 167, 31, 26],
@@ -19,7 +19,7 @@ const { selectedButton, selectorPosition } = useButtonNavigation({
}, },
initialButton: "projects", initialButton: "projects",
onButtonClick: (button) => { onButtonClick: (button) => {
if (button === "downloadPlay" || button === "theme" || button === "alarm") if (button === "theme" || button === "alarm")
throw new Error(`Not implemented: ${button}`); throw new Error(`Not implemented: ${button}`);
store.animateOutro(button); store.animateOutro(button);
@@ -28,15 +28,15 @@ const { selectedButton, selectorPosition } = useButtonNavigation({
projects: { projects: {
down: "last", down: "last",
left: "contact", left: "contact",
right: "downloadPlay", right: "gallery",
horizontalMode: "preview", horizontalMode: "preview",
}, },
contact: { contact: {
up: "projects", up: "projects",
right: "downloadPlay", right: "gallery",
down: "settings", down: "settings",
}, },
downloadPlay: { gallery: {
up: "projects", up: "projects",
left: "contact", left: "contact",
down: "settings", down: "settings",
@@ -70,6 +70,19 @@ const getOpacity = (button?: (typeof selectedButton)["value"]) => {
onRender((ctx) => { onRender((ctx) => {
ctx.globalAlpha = getOpacity(); ctx.globalAlpha = getOpacity();
// gallery
ctx.font = "7px NDS7";
ctx.fillStyle = "#2c2c2c";
fillTextCentered(
ctx,
$t("home.photoGallery"),
132,
78 + getButtonOffset("gallery"),
87,
);
// gba thing
ctx.font = "10px NDS10"; ctx.font = "10px NDS10";
ctx.fillStyle = "#a2a2a2"; ctx.fillStyle = "#a2a2a2";
fillTextCentered(ctx, $t("home.greeting"), 79, 135, 140); fillTextCentered(ctx, $t("home.greeting"), 79, 135, 140);
@@ -91,9 +104,9 @@ onRender((ctx) => {
/> />
<Button <Button
:x="128" :x="128"
:y="72 + getButtonOffset('downloadPlay')" :y="72 + getButtonOffset('gallery')"
:opacity="getOpacity('downloadPlay')" :opacity="getOpacity('gallery')"
:image="assets.home.bottomScreen.buttons.downloadPlay" :image="assets.home.bottomScreen.buttons.gallery"
/> />
<Button <Button

View File

@@ -41,6 +41,15 @@ const { camera, renderer } = useTresContext();
model.scale.set(100, 100, 100); model.scale.set(100, 100, 100);
const app = useAppStore();
watch(
() => camera.activeCamera.value,
(cam) => {
if (cam) app.setCamera(cam);
},
{ immediate: true },
);
meshes.clear(); meshes.clear();
model.traverse((child) => { model.traverse((child) => {
if (child instanceof THREE.Mesh) { if (child instanceof THREE.Mesh) {
@@ -199,6 +208,7 @@ const handleClick = (event: MouseEvent) => {
switch (intersection.object.name) { switch (intersection.object.name) {
case TOP_SCREEN: case TOP_SCREEN:
case BOTTOM_SCREEN: { case BOTTOM_SCREEN: {
console.log(intersection);
const canvas = const canvas =
intersection.object.name === TOP_SCREEN intersection.object.name === TOP_SCREEN
? props.topScreenCanvas ? props.topScreenCanvas

View File

@@ -133,6 +133,8 @@ const animateIntro = async () => {
); );
}; };
const galleryStore = useGalleryStore();
const animateOutro = async () => { const animateOutro = async () => {
isAnimating.value = true; isAnimating.value = true;
@@ -156,6 +158,7 @@ const animateOutro = async () => {
typeText(descriptionText, DESCRIPTION, DESCRIPTION_DURATION, true, { typeText(descriptionText, DESCRIPTION, DESCRIPTION_DURATION, true, {
onComplete: async () => { onComplete: async () => {
await sleep(ANIMATION_SLEEP * 1000); await sleep(ANIMATION_SLEEP * 1000);
galleryStore.shouldAnimateOutro = true;
router.push("/"); router.push("/");
}, },
}); });

View File

@@ -1,8 +1,8 @@
<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";
// TODO: keys doesn't work anymore if 3D modea is disabled const ENABLE_3D = true;
const ENABLE_3D = false;
type ScreenInstance = InstanceType<typeof NDSScreen>; type ScreenInstance = InstanceType<typeof NDSScreen>;
@@ -59,7 +59,7 @@ useKeyUp((key) => {
<TresPerspectiveCamera <TresPerspectiveCamera
:args="[45, 1, 0.001, 1000]" :args="[45, 1, 0.001, 1000]"
:position="[0, 10, 12]" :position="[0, 10, 12]"
:rotation="[-Math.PI / 4.7, 0, 0]" :rotation="[THREE.MathUtils.degToRad(-38.3), 0, 0]"
/> />
<TresAmbientLight /> <TresAmbientLight />
@@ -79,6 +79,7 @@ useKeyUp((key) => {
<ContactTopScreen v-else-if="app.screen === 'contact'" /> <ContactTopScreen v-else-if="app.screen === 'contact'" />
<ProjectsTopScreen v-else-if="app.screen === 'projects'" /> <ProjectsTopScreen v-else-if="app.screen === 'projects'" />
<SettingsTopScreen v-else-if="app.screen === 'settings'" /> <SettingsTopScreen v-else-if="app.screen === 'settings'" />
<GalleryTopScreen v-else-if="app.screen === 'gallery'" />
</Screen> </Screen>
</div> </div>
<div> <div>
@@ -87,6 +88,7 @@ useKeyUp((key) => {
<ContactBottomScreen v-else-if="app.screen === 'contact'" /> <ContactBottomScreen v-else-if="app.screen === 'contact'" />
<ProjectsBottomScreen v-else-if="app.screen === 'projects'" /> <ProjectsBottomScreen v-else-if="app.screen === 'projects'" />
<SettingsBottomScreen v-else-if="app.screen === 'settings'" /> <SettingsBottomScreen v-else-if="app.screen === 'settings'" />
<GalleryBottomScreen v-else-if="app.screen === 'gallery'" />
</Screen> </Screen>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { z } from "zod/mini"; import { z } from "zod/mini";
import type * as THREE from "three";
const STORAGE_ID = "app_settings"; const STORAGE_ID = "app_settings";
@@ -29,6 +30,7 @@ export const useAppStore = defineStore("app", {
booted: false, booted: false,
settings, settings,
screen: "home" as AppScreen, screen: "home" as AppScreen,
camera: null as THREE.Camera | null,
}; };
}, },
@@ -41,6 +43,10 @@ export const useAppStore = defineStore("app", {
this.screen = screen; this.screen = screen;
}, },
setCamera(camera: THREE.Camera) {
this.camera = camera;
},
save() { save() {
localStorage.setItem(STORAGE_ID, JSON.stringify(this.settings)); localStorage.setItem(STORAGE_ID, JSON.stringify(this.settings));
}, },

147
app/stores/gallery.ts Normal file
View File

@@ -0,0 +1,147 @@
import gsap from "gsap";
import * as THREE from "three";
export const useGalleryStore = defineStore("gallery", {
state: () => ({
intro: {
fadeOpacity: 0,
},
outro: {
fadeOpacity: 1,
},
isIntro: true,
isOutro: false,
shouldAnimateOutro: false,
}),
actions: {
animateIntro() {
const CAMERA_DELAY = 0.7;
const CAMERA_DURATION = 3;
const FADE_DELAY = 0;
const FADE_DURATION = 1;
this.isIntro = true;
this.isOutro = false;
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(
app.camera.rotation,
{
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(
this.intro,
{ fadeOpacity: 0 },
{
fadeOpacity: 1,
duration: FADE_DURATION,
delay: FADE_DELAY,
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) {
gsap.fromTo(
app.camera.position,
{
x: 0,
y: 3,
z: -1.7,
},
{
x: 0,
y: 10,
z: 12,
duration: CAMERA_DURATION,
delay: CAMERA_DELAY,
ease: "power2.inOut",
},
);
gsap.fromTo(
app.camera.rotation,
{
x: THREE.MathUtils.degToRad(-25),
y: 0,
z: 0,
},
{
x: THREE.MathUtils.degToRad(-38.3),
y: 0,
z: 0,
duration: CAMERA_DURATION * 0.9,
delay: CAMERA_DELAY,
ease: "power2.inOut",
},
);
}
gsap.fromTo(
this.outro,
{ fadeOpacity: 1 },
{
fadeOpacity: 0,
duration: FADE_DURATION,
delay: FADE_DELAY,
ease: "none",
},
);
setTimeout(() => {
this.isOutro = false;
app.navigateTo("home");
}, 3310);
},
},
});

View File

@@ -61,8 +61,13 @@ export const useHomeStore = defineStore("home", {
const timeline = gsap.timeline({ const timeline = gsap.timeline({
onComplete: () => { onComplete: () => {
this.isOutro = true; this.isOutro = true;
const app = useAppStore(); const app = useAppStore();
if (to === "gallery") {
app.navigateTo("gallery");
} else {
app.navigateTo(to); app.navigateTo(to);
}
}, },
}); });

2
app/types/app.d.ts vendored
View File

@@ -1 +1 @@
type AppScreen = "home" | "contact" | "projects" | "settings"; type AppScreen = "home" | "contact" | "projects" | "settings" | "gallery";

View File

@@ -1,6 +1,7 @@
{ {
"home": { "home": {
"greeting": "Welcome to my website!" "greeting": "Welcome to my website!",
"photoGallery": "Photo Gallery"
}, },
"settings": { "settings": {
"title": "Settings", "title": "Settings",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B