feat(intro): implement intro screen
@@ -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");
|
||||
|
||||
BIN
app/assets/fonts/nds-12px-bold.woff2
Normal 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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
100
app/components/Intro/BottomScreen/BottomScreen.vue
Normal 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>
|
||||
35
app/components/Intro/TopScreen/TopScreen.vue
Normal 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>
|
||||
@@ -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
@@ -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;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
BIN
public/nds/images/intro/logo-animated/01.webp
Normal file
|
After Width: | Height: | Size: 136 B |
BIN
public/nds/images/intro/logo-animated/02.webp
Normal file
|
After Width: | Height: | Size: 190 B |
BIN
public/nds/images/intro/logo-animated/03.webp
Normal file
|
After Width: | Height: | Size: 178 B |
BIN
public/nds/images/intro/logo-animated/04.webp
Normal file
|
After Width: | Height: | Size: 566 B |
BIN
public/nds/images/intro/logo-animated/05.webp
Normal file
|
After Width: | Height: | Size: 648 B |
BIN
public/nds/images/intro/logo-animated/06.webp
Normal file
|
After Width: | Height: | Size: 642 B |
BIN
public/nds/images/intro/logo-animated/07.webp
Normal file
|
After Width: | Height: | Size: 678 B |
BIN
public/nds/images/intro/logo-animated/08.webp
Normal file
|
After Width: | Height: | Size: 654 B |
BIN
public/nds/images/intro/logo-animated/09.webp
Normal file
|
After Width: | Height: | Size: 668 B |
BIN
public/nds/images/intro/logo-animated/10.webp
Normal file
|
After Width: | Height: | Size: 848 B |
BIN
public/nds/images/intro/logo-animated/11.webp
Normal file
|
After Width: | Height: | Size: 912 B |
BIN
public/nds/images/intro/logo-animated/12.webp
Normal file
|
After Width: | Height: | Size: 956 B |
BIN
public/nds/images/intro/logo-animated/13.webp
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
public/nds/images/intro/logo-animated/14.webp
Normal file
|
After Width: | Height: | Size: 944 B |
BIN
public/nds/images/intro/logo-animated/15.webp
Normal file
|
After Width: | Height: | Size: 860 B |
BIN
public/nds/images/intro/logo-animated/16.webp
Normal file
|
After Width: | Height: | Size: 990 B |
BIN
public/nds/images/intro/logo-animated/17.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/nds/images/intro/logo-animated/18.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/nds/images/intro/logo-animated/19.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/nds/images/intro/logo-animated/20.webp
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/nds/images/intro/logo-animated/21.webp
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/nds/images/intro/logo-animated/22.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/nds/images/intro/logo-animated/23.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/nds/images/intro/logo-animated/24.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/nds/images/intro/logo-animated/25.webp
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/nds/images/intro/logo-animated/26.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/nds/images/intro/logo-animated/27.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
public/nds/images/intro/logo-animated/28.webp
Normal file
|
After Width: | Height: | Size: 994 B |
BIN
public/nds/images/intro/logo-animated/29.webp
Normal file
|
After Width: | Height: | Size: 1002 B |
BIN
public/nds/images/intro/logo-animated/30.webp
Normal file
|
After Width: | Height: | Size: 986 B |
BIN
public/nds/images/intro/logo-animated/31.webp
Normal file
|
After Width: | Height: | Size: 1000 B |
BIN
public/nds/images/intro/logo-animated/32.webp
Normal file
|
After Width: | Height: | Size: 814 B |
BIN
public/nds/images/intro/logo-animated/33.webp
Normal file
|
After Width: | Height: | Size: 734 B |
BIN
public/nds/images/intro/logo-animated/34.webp
Normal file
|
After Width: | Height: | Size: 724 B |
BIN
public/nds/images/intro/logo-animated/35.webp
Normal file
|
After Width: | Height: | Size: 718 B |
BIN
public/nds/images/intro/logo-animated/36.webp
Normal file
|
After Width: | Height: | Size: 714 B |
BIN
public/nds/images/intro/logo-animated/37.webp
Normal file
|
After Width: | Height: | Size: 724 B |
BIN
public/nds/images/intro/logo-animated/38.webp
Normal file
|
After Width: | Height: | Size: 714 B |
BIN
public/nds/images/intro/logo-animated/39.webp
Normal file
|
After Width: | Height: | Size: 674 B |
BIN
public/nds/images/intro/warning.webp
Normal file
|
After Width: | Height: | Size: 92 B |