feat(intro): implement intro screen

This commit is contained in:
2026-01-12 16:13:01 +01:00
parent 3ecd2a9019
commit 130bafa7dc
48 changed files with 218 additions and 5 deletions

View File

@@ -17,6 +17,13 @@
font-style: normal;
}
@font-face {
font-family: "NDS12 Bold";
src: url("/assets/fonts/nds-12px-bold.woff2") format("woff2");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "NDS39";
src: url("/assets/fonts/nds-39px.ttf") format("truetype");

Binary file not shown.

View File

@@ -7,7 +7,10 @@ const app = useAppStore();
const { assets } = useAssets();
onRender((ctx) => {
ctx.globalAlpha = app.booted ? 1 : store.intro.stage1Opacity;
ctx.fillStyle = "#fbfbfb"
ctx.fillRect(0, 0, LOGICAL_WIDTH, LOGICAL_HEIGHT);
ctx.globalAlpha = store.isIntro? store.intro.stage1Opacity:1;
assets.images.home.bottomScreen.background.draw(ctx, 0, 0);
});

View File

@@ -6,7 +6,10 @@ const app = useAppStore();
const { assets } = useAssets();
onRender((ctx) => {
ctx.globalAlpha = app.booted ? 1 : store.intro.stage1Opacity;
ctx.fillStyle = "#fbfbfb"
ctx.fillRect(0, 0, LOGICAL_WIDTH, LOGICAL_HEIGHT);
ctx.globalAlpha = store.isIntro? store.intro.stage1Opacity:1;
assets.images.home.topScreen.background.draw(ctx, 0, 0);
});

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import gsap from "gsap";
const app = useAppStore();
const store = useIntroStore();
const { onRender, onClick } = useScreen();
const { assets } = useAssets();
const TITLE_Y = 25;
const TEXT_Y = 63;
const HINT_Y = 167;
const hintTextOpacity = ref(0);
store.$subscribe((_, state) => {
if (!state.isIntro && !state.isOutro) {
gsap.to(hintTextOpacity, {
value: 1,
duration: 0.5,
repeat: -1,
yoyo: true,
ease: "none",
});
}
});
onRender((ctx) => {
ctx.textBaseline = "top";
ctx.fillStyle = "#fbfbfb";
ctx.fillRect(0, 0, LOGICAL_WIDTH, LOGICAL_HEIGHT);
ctx.fillStyle = "#000000";
// title
ctx.font = "12px NDS12 Bold";
ctx.letterSpacing = "1px";
const TEXT = "WARNING - COPYRIGHT";
const { actualBoundingBoxRight: width } = ctx.measureText(TEXT);
const logoWidth = assets.images.intro.warning.rect.width;
const logoX = Math.floor(LOGICAL_WIDTH / 2 - (width + logoWidth + 3) / 2);
ctx.globalAlpha = store.isIntro
? store.intro.textOpacity
: store.isOutro
? store.outro.textOpacity
: 1;
assets.images.intro.warning.draw(ctx, logoX, TITLE_Y - 1);
ctx.fillText(TEXT, logoX + logoWidth + 3, TITLE_Y);
ctx.letterSpacing = "0px";
// text
ctx.font = "10px NDS10";
const TEXT2 =
"THIS IS A NON-COMMERCIAL FAN-MADE\nRECREATION. NOT AFFILIATED WITH\nOR ENDORSED BY NINTENDO.\nNINTENDO DS IS A TRADEMARK OF\nNINTENDO CO., LTD.";
const lines = TEXT2.split("\n");
for (let i = 0, y = TEXT_Y; i < lines.length; i += 1, y += 18)
fillTextHCentered(ctx, lines[i]!, 0, y, LOGICAL_WIDTH);
// hint
ctx.font = "10px NDS10";
ctx.globalAlpha = store.isIntro
? 0
: store.isOutro
? store.outro.textOpacity
: hintTextOpacity.value;
fillTextHCentered(
ctx,
"Touch the Touch Screen to continue.",
0,
HINT_Y,
LOGICAL_WIDTH,
);
});
onClick(() => {
if (store.isIntro || store.isOutro) return;
store.animateOutro();
});
useKeyDown((key) => {
if (store.isIntro || store.isOutro) return;
switch (key) {
case "NDS_A":
case "NDS_START":
store.animateOutro();
break;
}
});
</script>
<template>
<div />
</template>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
const { onRender } = useScreen();
const { assets } = useAssets();
const store = useIntroStore();
const frames = Object.keys(assets.images.intro.logoAnimated).sort();
onMounted(() => {
store.$reset();
store.animateIntro();
});
onRender((ctx) => {
ctx.fillStyle = "#fbfbfb";
ctx.fillRect(0, 0, LOGICAL_WIDTH, LOGICAL_HEIGHT);
ctx.globalAlpha = store.isIntro
? store.intro.textOpacity
: store.isOutro
? store.outro.textOpacity
: 1;
const frameIndex = Math.floor(store.intro.logoFrameIndex);
const frameKey = frames[
frameIndex
] as keyof typeof assets.images.intro.logoAnimated;
const frame = assets.images.intro.logoAnimated[frameKey];
frame.draw(ctx, 0, 0);
});
</script>
<template>
<div />
</template>

View File

@@ -20,14 +20,11 @@ export const useHomeStore = defineStore("home", {
actions: {
animateIntro() {
const appStore = useAppStore();
this.isIntro = true;
const timeline = gsap.timeline({
onComplete: () => {
this.isIntro = false;
if (!appStore.booted) appStore.booted = true;
},
});

68
app/stores/intro.ts Normal file
View File

@@ -0,0 +1,68 @@
import gsap from "gsap";
export const useIntroStore = defineStore("intro", {
state: () => ({
intro: {
textOpacity: 0,
logoFrameIndex: 0,
},
outro: {
textOpacity: 1,
},
isIntro: true,
isOutro: false,
}),
actions: {
animateIntro() {
this.isIntro = true;
const { assets } = useAssets();
const totalFrames = Object.keys(assets.images.intro.logoAnimated).length;
const logoDuration = totalFrames / 25;
gsap
.timeline()
.to({}, { duration: 2 })
.to(
this.intro,
{
textOpacity: 1,
duration: 0.1,
ease: "none",
},
3,
)
.to(
this.intro,
{
logoFrameIndex: totalFrames - 1,
duration: logoDuration,
ease: "steps(" + (totalFrames - 1) + ")",
},
3,
)
.call(() => {
this.isIntro = false;
});
},
animateOutro() {
this.isOutro = true;
gsap
.timeline()
.to(this.outro, {
textOpacity: 0,
duration: 0.25,
ease: "none",
})
.call(() => {
const app = useAppStore();
app.booted = true;
});
},
},
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1002 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B