feat(intro): implement intro screen
@@ -17,6 +17,13 @@
|
|||||||
font-style: normal;
|
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-face {
|
||||||
font-family: "NDS39";
|
font-family: "NDS39";
|
||||||
src: url("/assets/fonts/nds-39px.ttf") format("truetype");
|
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();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
onRender((ctx) => {
|
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);
|
assets.images.home.bottomScreen.background.draw(ctx, 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ const app = useAppStore();
|
|||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
onRender((ctx) => {
|
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);
|
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: {
|
actions: {
|
||||||
animateIntro() {
|
animateIntro() {
|
||||||
const appStore = useAppStore();
|
|
||||||
|
|
||||||
this.isIntro = true;
|
this.isIntro = true;
|
||||||
|
|
||||||
const timeline = gsap.timeline({
|
const timeline = gsap.timeline({
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
this.isIntro = false;
|
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 |