feat(home): intro + outro animations
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 193 KiB |
BIN
app/assets/images/home/top-screen/calendar/calendar.png
Normal file
BIN
app/assets/images/home/top-screen/calendar/calendar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
app/assets/images/home/top-screen/clock.png
Normal file
BIN
app/assets/images/home/top-screen/clock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
app/assets/images/home/top-screen/status-bar/status-bar.png
Normal file
BIN
app/assets/images/home/top-screen/status-bar/status-bar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -1,9 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const store = useHomeStore();
|
||||||
|
|
||||||
const backgroundImage = useTemplateRef("backgroundImage");
|
const backgroundImage = useTemplateRef("backgroundImage");
|
||||||
|
|
||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
if (!backgroundImage.value) return;
|
if (!backgroundImage.value) return;
|
||||||
|
|
||||||
|
ctx.globalAlpha = store.intro.stage1Opacity;
|
||||||
ctx.drawImage(backgroundImage.value, 0, 0);
|
ctx.drawImage(backgroundImage.value, 0, 0);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import gsap from "gsap";
|
// the out transition:
|
||||||
|
// clicked button goes up
|
||||||
|
// other buttons, including selector fade away, text + status bar of upper screen and clock hands fades away 2x faster
|
||||||
|
// other buttons + calendar + clock body finish fading away at the same time
|
||||||
|
// then we wait a bit, and new screen starts appearing
|
||||||
|
|
||||||
import GameButton from "./GameButton.vue";
|
import GameButton from "./GameButton.vue";
|
||||||
import PictochatButton from "./PictochatButton.vue";
|
import PictochatButton from "./PictochatButton.vue";
|
||||||
import DownloadPlayButton from "./DownloadPlayButton.vue";
|
import DownloadPlayButton from "./DownloadPlayButton.vue";
|
||||||
import Selector from "./Selector.vue";
|
import Selector from "./Selector.vue";
|
||||||
|
|
||||||
|
const store = useHomeStore();
|
||||||
|
|
||||||
const BUTTONS_CONFIG = {
|
const BUTTONS_CONFIG = {
|
||||||
game: [31, 23, 193, 49],
|
game: [31, 23, 193, 49],
|
||||||
pictochat: [31, 71, 97, 49],
|
pictochat: [31, 71, 97, 49],
|
||||||
@@ -13,29 +20,11 @@ const BUTTONS_CONFIG = {
|
|||||||
|
|
||||||
type ButtonType = keyof typeof BUTTONS_CONFIG;
|
type ButtonType = keyof typeof BUTTONS_CONFIG;
|
||||||
|
|
||||||
const animationPercentage = ref(0);
|
|
||||||
|
|
||||||
const { selectedButton, selectorPosition } = useButtonNavigation({
|
const { selectedButton, selectorPosition } = useButtonNavigation({
|
||||||
buttons: BUTTONS_CONFIG,
|
buttons: BUTTONS_CONFIG,
|
||||||
initialButton: "game",
|
initialButton: "game",
|
||||||
onButtonClick: (buttonName) => {
|
onButtonClick: () => {
|
||||||
if (animationPercentage.value > 0) return;
|
store.animateOutro();
|
||||||
gsap.fromTo(
|
|
||||||
animationPercentage,
|
|
||||||
{ value: 0 },
|
|
||||||
{
|
|
||||||
value: 1,
|
|
||||||
duration: 0.35,
|
|
||||||
ease: "none",
|
|
||||||
onComplete: () => {
|
|
||||||
if (buttonName === "pictochat") {
|
|
||||||
navigateTo("/contact");
|
|
||||||
} else {
|
|
||||||
throw new Error(`Not implemented: ${buttonName}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
game: {
|
game: {
|
||||||
@@ -56,28 +45,32 @@ const { selectedButton, selectorPosition } = useButtonNavigation({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getButtonOffset = (button: ButtonType) => {
|
const getButtonOffset = (button: ButtonType) => {
|
||||||
if (selectedButton.value === button) return animationPercentage.value * -200;
|
if (selectedButton.value === button) return store.outro.buttonOffsetY;
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOpacity = () => (1 - animationPercentage.value) * 100;
|
const getOpacity = (button?: ButtonType) => {
|
||||||
|
if (store.isIntro) return store.intro.stage1Opacity;
|
||||||
|
if (selectedButton.value === button) return 1;
|
||||||
|
return store.outro.stage1Opacity;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<GameButton
|
<GameButton
|
||||||
:x="33"
|
:x="33"
|
||||||
:y="25 + getButtonOffset('game')"
|
:y="25 + getButtonOffset('game')"
|
||||||
:opacity="getOpacity()"
|
:opacity="getOpacity('game')"
|
||||||
/>
|
/>
|
||||||
<DownloadPlayButton
|
<DownloadPlayButton
|
||||||
:x="128"
|
:x="128"
|
||||||
:y="72 + getButtonOffset('downloadPlay')"
|
:y="72 + getButtonOffset('downloadPlay')"
|
||||||
:opacity="getOpacity()"
|
:opacity="getOpacity('downloadPlay')"
|
||||||
/>
|
/>
|
||||||
<PictochatButton
|
<PictochatButton
|
||||||
:x="32"
|
:x="32"
|
||||||
:y="72 + getButtonOffset('pictochat')"
|
:y="72 + getButtonOffset('pictochat')"
|
||||||
:opacity="getOpacity()"
|
:opacity="getOpacity('pictochat')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Selector
|
<Selector
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const buttonImage = useTemplateRef("buttonImage");
|
|||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
if (!buttonImage.value) return;
|
if (!buttonImage.value) return;
|
||||||
|
|
||||||
ctx.globalAlpha = props.opacity / 100;
|
ctx.globalAlpha = props.opacity;
|
||||||
ctx.drawImage(buttonImage.value, props.x, props.y);
|
ctx.drawImage(buttonImage.value, props.x, props.y);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const buttonImage = useTemplateRef("buttonImage");
|
|||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
if (!buttonImage.value) return;
|
if (!buttonImage.value) return;
|
||||||
|
|
||||||
ctx.globalAlpha = props.opacity / 100;
|
ctx.globalAlpha = props.opacity;
|
||||||
ctx.drawImage(buttonImage.value, props.x, props.y);
|
ctx.drawImage(buttonImage.value, props.x, props.y);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const buttonImage = useTemplateRef("buttonImage");
|
|||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
if (!buttonImage.value) return;
|
if (!buttonImage.value) return;
|
||||||
|
|
||||||
ctx.globalAlpha = props.opacity / 100;
|
ctx.globalAlpha = props.opacity;
|
||||||
ctx.drawImage(buttonImage.value, props.x, props.y);
|
ctx.drawImage(buttonImage.value, props.x, props.y);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ useRender((ctx) => {
|
|||||||
const w = Math.floor(currentWidth);
|
const w = Math.floor(currentWidth);
|
||||||
const h = Math.floor(currentHeight);
|
const h = Math.floor(currentHeight);
|
||||||
|
|
||||||
ctx.globalAlpha = props.opacity / 100;
|
ctx.globalAlpha = props.opacity;
|
||||||
|
|
||||||
ctx.drawImage(cornerImage.value, x, y);
|
ctx.drawImage(cornerImage.value, x, y);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const store = useHomeStore();
|
||||||
|
|
||||||
const backgroundImage = useTemplateRef("backgroundImage");
|
const backgroundImage = useTemplateRef("backgroundImage");
|
||||||
|
|
||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
if (!backgroundImage.value) return;
|
if (!backgroundImage.value) return;
|
||||||
|
|
||||||
|
ctx.globalAlpha = store.intro.stage1Opacity;
|
||||||
|
|
||||||
ctx.drawImage(backgroundImage.value, 0, 0);
|
ctx.drawImage(backgroundImage.value, 0, 0);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// NOTE: calendar background is handled by TopScreenBackground
|
// NOTE: calendar background is handled by TopScreenBackground
|
||||||
|
const store = useHomeStore();
|
||||||
|
|
||||||
const lastRowImage = useTemplateRef("lastRowImage");
|
const lastRowImage = useTemplateRef("lastRowImage");
|
||||||
const daySelectorImage = useTemplateRef("daySelectorImage");
|
const daySelectorImage = useTemplateRef("daySelectorImage");
|
||||||
|
const calendarImage = useTemplateRef("calendarImage");
|
||||||
|
|
||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
if (!lastRowImage.value || !daySelectorImage.value) return;
|
if (!calendarImage.value || !lastRowImage.value || !daySelectorImage.value)
|
||||||
|
return;
|
||||||
|
|
||||||
ctx.fillStyle = "black";
|
ctx.fillStyle = "black";
|
||||||
ctx.font = "7px NDS7";
|
ctx.font = "7px NDS7";
|
||||||
@@ -24,11 +27,19 @@ useRender((ctx) => {
|
|||||||
const firstDay = new Date(year, month, 1).getDay();
|
const firstDay = new Date(year, month, 1).getDay();
|
||||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
|
||||||
|
ctx.globalAlpha = store.isIntro
|
||||||
|
? store.intro.stage1Opacity
|
||||||
|
: store.outro.stage1Opacity;
|
||||||
|
ctx.drawImage(calendarImage.value, CALENDAR_LEFT - 3, CALENDAR_TOP - 33);
|
||||||
|
|
||||||
const extraRow = CALENDAR_COLS * CALENDAR_ROWS - daysInMonth - firstDay < 0;
|
const extraRow = CALENDAR_COLS * CALENDAR_ROWS - daysInMonth - firstDay < 0;
|
||||||
if (extraRow) {
|
if (extraRow) {
|
||||||
ctx.drawImage(lastRowImage.value, CALENDAR_LEFT - 3, CALENDAR_TOP + 79);
|
ctx.drawImage(lastRowImage.value, CALENDAR_LEFT - 3, CALENDAR_TOP + 79);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = store.isIntro
|
||||||
|
? store.intro.stage1Opacity
|
||||||
|
: store.outro.stage2Opacity;
|
||||||
for (let col = 0; col < CALENDAR_ROWS + (extraRow ? 1 : 0); col += 1) {
|
for (let col = 0; col < CALENDAR_ROWS + (extraRow ? 1 : 0); col += 1) {
|
||||||
for (let row = 0; row < CALENDAR_COLS; row += 1) {
|
for (let row = 0; row < CALENDAR_COLS; row += 1) {
|
||||||
const cellIndex = col * CALENDAR_COLS + row;
|
const cellIndex = col * CALENDAR_COLS + row;
|
||||||
@@ -70,6 +81,11 @@ useRender((ctx) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<img
|
||||||
|
ref="calendarImage"
|
||||||
|
src="/assets/images/home/top-screen/calendar/calendar.png"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
<img
|
<img
|
||||||
ref="lastRowImage"
|
ref="lastRowImage"
|
||||||
src="/assets/images/home/top-screen/calendar/last-row.png"
|
src="/assets/images/home/top-screen/calendar/last-row.png"
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
const CENTER_X = 63;
|
const CENTER_X = 63;
|
||||||
const CENTER_Y = 95;
|
const CENTER_Y = 95;
|
||||||
|
|
||||||
|
const store = useHomeStore();
|
||||||
|
|
||||||
|
const clockImage = useTemplateRef("clockImage");
|
||||||
|
|
||||||
function drawLine(
|
function drawLine(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
x0: number,
|
x0: number,
|
||||||
@@ -48,6 +52,16 @@ function drawLine(
|
|||||||
}
|
}
|
||||||
|
|
||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
|
if (!clockImage.value) return;
|
||||||
|
|
||||||
|
ctx.globalAlpha = store.isIntro
|
||||||
|
? store.intro.stage1Opacity
|
||||||
|
: store.outro.stage1Opacity;
|
||||||
|
ctx.drawImage(clockImage.value, 13, 45);
|
||||||
|
|
||||||
|
ctx.globalAlpha = store.isIntro
|
||||||
|
? store.intro.stage1Opacity
|
||||||
|
: store.outro.stage2Opacity;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const renderHand = (
|
const renderHand = (
|
||||||
@@ -75,8 +89,8 @@ useRender((ctx) => {
|
|||||||
ctx.fillStyle = "#494949";
|
ctx.fillStyle = "#494949";
|
||||||
ctx.fillRect(CENTER_X - 2, CENTER_Y - 2, 5, 5);
|
ctx.fillRect(CENTER_X - 2, CENTER_Y - 2, 5, 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
render: () => null,
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<img ref="clockImage" src="/assets/images/home/top-screen/clock.png" hidden />
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const store = useHomeStore();
|
||||||
|
|
||||||
|
const statusBarImage = useTemplateRef("statusBarImage");
|
||||||
const gbaDisplayImage = useTemplateRef("gbaDisplayImage");
|
const gbaDisplayImage = useTemplateRef("gbaDisplayImage");
|
||||||
const startupModeImage = useTemplateRef("startupModeImage");
|
const startupModeImage = useTemplateRef("startupModeImage");
|
||||||
const batteryImage = useTemplateRef("batteryImage");
|
const batteryImage = useTemplateRef("batteryImage");
|
||||||
|
|
||||||
|
// TODO: don't call it here
|
||||||
|
callOnce("intro", () => store.animateIntro());
|
||||||
|
|
||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
if (!gbaDisplayImage.value || !startupModeImage.value || !batteryImage.value)
|
if (
|
||||||
|
!statusBarImage.value ||
|
||||||
|
!gbaDisplayImage.value ||
|
||||||
|
!startupModeImage.value ||
|
||||||
|
!batteryImage.value
|
||||||
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const TEXT_Y = 11;
|
const TEXT_Y = 11;
|
||||||
|
|
||||||
|
ctx.translate(0, store.intro.statusBarY);
|
||||||
|
|
||||||
|
ctx.globalAlpha = store.outro.stage2Opacity;
|
||||||
|
ctx.drawImage(statusBarImage.value, 0, 0);
|
||||||
|
|
||||||
ctx.fillStyle = "#ffffff";
|
ctx.fillStyle = "#ffffff";
|
||||||
ctx.font = "7px NDS7";
|
ctx.font = "7px NDS7";
|
||||||
|
|
||||||
@@ -44,6 +60,11 @@ useRender((ctx) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<img
|
||||||
|
ref="statusBarImage"
|
||||||
|
src="/assets/images/home/top-screen/status-bar/status-bar.png"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
<img
|
<img
|
||||||
ref="gbaDisplayImage"
|
ref="gbaDisplayImage"
|
||||||
src="/assets/images/home/top-screen/status-bar/gba-display.png"
|
src="/assets/images/home/top-screen/status-bar/gba-display.png"
|
||||||
|
|||||||
91
app/stores/home.ts
Normal file
91
app/stores/home.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import gsap from "gsap";
|
||||||
|
|
||||||
|
export const useHomeStore = defineStore("home", {
|
||||||
|
state: () => ({
|
||||||
|
intro: {
|
||||||
|
statusBarY: -20,
|
||||||
|
stage1Opacity: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
outro: {
|
||||||
|
buttonOffsetY: 0,
|
||||||
|
stage1Opacity: 1,
|
||||||
|
stage2Opacity: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
isIntro: false,
|
||||||
|
isOutro: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
animateIntro() {
|
||||||
|
this.isIntro = true;
|
||||||
|
|
||||||
|
const start = 0.5;
|
||||||
|
const duration = 0.5;
|
||||||
|
const delay = 0.35;
|
||||||
|
|
||||||
|
gsap.fromTo(
|
||||||
|
this.intro,
|
||||||
|
{ stage1Opacity: 0 },
|
||||||
|
{
|
||||||
|
stage1Opacity: 1,
|
||||||
|
duration,
|
||||||
|
delay: start,
|
||||||
|
ease: "none",
|
||||||
|
onComplete: () => {
|
||||||
|
this.isIntro = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
gsap.fromTo(
|
||||||
|
this.intro,
|
||||||
|
{ statusBarY: -20 },
|
||||||
|
{
|
||||||
|
statusBarY: 0,
|
||||||
|
duration: duration - delay,
|
||||||
|
delay: start + delay,
|
||||||
|
ease: "none",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
animateOutro() {
|
||||||
|
this.isOutro = true;
|
||||||
|
|
||||||
|
const duration = 0.4;
|
||||||
|
const delay = 0.08;
|
||||||
|
|
||||||
|
gsap.fromTo(
|
||||||
|
this.outro,
|
||||||
|
{
|
||||||
|
buttonOffsetY: 0,
|
||||||
|
stage1Opacity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
buttonOffsetY: -200,
|
||||||
|
stage1Opacity: 0,
|
||||||
|
duration,
|
||||||
|
delay,
|
||||||
|
ease: "none",
|
||||||
|
onComplete: () => {
|
||||||
|
this.isOutro = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
gsap.fromTo(
|
||||||
|
this.outro,
|
||||||
|
{
|
||||||
|
stage2Opacity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage2Opacity: 0,
|
||||||
|
ease: "none",
|
||||||
|
duration: duration / 2 - delay,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2025-07-15",
|
compatibilityDate: "2025-07-15",
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
modules: ["@nuxt/eslint"],
|
modules: ["@nuxt/eslint", "@pinia/nuxt"],
|
||||||
css: ["~/assets/app.css"],
|
css: ["~/assets/app.css"],
|
||||||
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,8 +12,10 @@
|
|||||||
"lint": "eslint --fix --cache ."
|
"lint": "eslint --fix --cache ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@pinia/nuxt": "0.11.3",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"nuxt": "^4.2.1",
|
"nuxt": "^4.2.1",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
|
|||||||
7633
pnpm-lock.yaml
generated
7633
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user