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">
|
||||
const store = useHomeStore();
|
||||
|
||||
const backgroundImage = useTemplateRef("backgroundImage");
|
||||
|
||||
useRender((ctx) => {
|
||||
if (!backgroundImage.value) return;
|
||||
|
||||
ctx.globalAlpha = store.intro.stage1Opacity;
|
||||
ctx.drawImage(backgroundImage.value, 0, 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
<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 PictochatButton from "./PictochatButton.vue";
|
||||
import DownloadPlayButton from "./DownloadPlayButton.vue";
|
||||
import Selector from "./Selector.vue";
|
||||
|
||||
const store = useHomeStore();
|
||||
|
||||
const BUTTONS_CONFIG = {
|
||||
game: [31, 23, 193, 49],
|
||||
pictochat: [31, 71, 97, 49],
|
||||
@@ -13,29 +20,11 @@ const BUTTONS_CONFIG = {
|
||||
|
||||
type ButtonType = keyof typeof BUTTONS_CONFIG;
|
||||
|
||||
const animationPercentage = ref(0);
|
||||
|
||||
const { selectedButton, selectorPosition } = useButtonNavigation({
|
||||
buttons: BUTTONS_CONFIG,
|
||||
initialButton: "game",
|
||||
onButtonClick: (buttonName) => {
|
||||
if (animationPercentage.value > 0) return;
|
||||
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}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
onButtonClick: () => {
|
||||
store.animateOutro();
|
||||
},
|
||||
navigation: {
|
||||
game: {
|
||||
@@ -56,28 +45,32 @@ const { selectedButton, selectorPosition } = useButtonNavigation({
|
||||
});
|
||||
|
||||
const getButtonOffset = (button: ButtonType) => {
|
||||
if (selectedButton.value === button) return animationPercentage.value * -200;
|
||||
if (selectedButton.value === button) return store.outro.buttonOffsetY;
|
||||
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>
|
||||
|
||||
<template>
|
||||
<GameButton
|
||||
:x="33"
|
||||
:y="25 + getButtonOffset('game')"
|
||||
:opacity="getOpacity()"
|
||||
:opacity="getOpacity('game')"
|
||||
/>
|
||||
<DownloadPlayButton
|
||||
:x="128"
|
||||
:y="72 + getButtonOffset('downloadPlay')"
|
||||
:opacity="getOpacity()"
|
||||
:opacity="getOpacity('downloadPlay')"
|
||||
/>
|
||||
<PictochatButton
|
||||
:x="32"
|
||||
:y="72 + getButtonOffset('pictochat')"
|
||||
:opacity="getOpacity()"
|
||||
:opacity="getOpacity('pictochat')"
|
||||
/>
|
||||
|
||||
<Selector
|
||||
|
||||
@@ -10,7 +10,7 @@ const buttonImage = useTemplateRef("buttonImage");
|
||||
useRender((ctx) => {
|
||||
if (!buttonImage.value) return;
|
||||
|
||||
ctx.globalAlpha = props.opacity / 100;
|
||||
ctx.globalAlpha = props.opacity;
|
||||
ctx.drawImage(buttonImage.value, props.x, props.y);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,7 @@ const buttonImage = useTemplateRef("buttonImage");
|
||||
useRender((ctx) => {
|
||||
if (!buttonImage.value) return;
|
||||
|
||||
ctx.globalAlpha = props.opacity / 100;
|
||||
ctx.globalAlpha = props.opacity;
|
||||
ctx.drawImage(buttonImage.value, props.x, props.y);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,7 @@ const buttonImage = useTemplateRef("buttonImage");
|
||||
useRender((ctx) => {
|
||||
if (!buttonImage.value) return;
|
||||
|
||||
ctx.globalAlpha = props.opacity / 100;
|
||||
ctx.globalAlpha = props.opacity;
|
||||
ctx.drawImage(buttonImage.value, props.x, props.y);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -46,7 +46,7 @@ useRender((ctx) => {
|
||||
const w = Math.floor(currentWidth);
|
||||
const h = Math.floor(currentHeight);
|
||||
|
||||
ctx.globalAlpha = props.opacity / 100;
|
||||
ctx.globalAlpha = props.opacity;
|
||||
|
||||
ctx.drawImage(cornerImage.value, x, y);
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
const store = useHomeStore();
|
||||
|
||||
const backgroundImage = useTemplateRef("backgroundImage");
|
||||
|
||||
useRender((ctx) => {
|
||||
if (!backgroundImage.value) return;
|
||||
|
||||
ctx.globalAlpha = store.intro.stage1Opacity;
|
||||
|
||||
ctx.drawImage(backgroundImage.value, 0, 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
// NOTE: calendar background is handled by TopScreenBackground
|
||||
const store = useHomeStore();
|
||||
|
||||
const lastRowImage = useTemplateRef("lastRowImage");
|
||||
const daySelectorImage = useTemplateRef("daySelectorImage");
|
||||
const calendarImage = useTemplateRef("calendarImage");
|
||||
|
||||
useRender((ctx) => {
|
||||
if (!lastRowImage.value || !daySelectorImage.value) return;
|
||||
if (!calendarImage.value || !lastRowImage.value || !daySelectorImage.value)
|
||||
return;
|
||||
|
||||
ctx.fillStyle = "black";
|
||||
ctx.font = "7px NDS7";
|
||||
@@ -24,11 +27,19 @@ useRender((ctx) => {
|
||||
const firstDay = new Date(year, month, 1).getDay();
|
||||
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;
|
||||
if (extraRow) {
|
||||
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 row = 0; row < CALENDAR_COLS; row += 1) {
|
||||
const cellIndex = col * CALENDAR_COLS + row;
|
||||
@@ -70,6 +81,11 @@ useRender((ctx) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
ref="calendarImage"
|
||||
src="/assets/images/home/top-screen/calendar/calendar.png"
|
||||
hidden
|
||||
/>
|
||||
<img
|
||||
ref="lastRowImage"
|
||||
src="/assets/images/home/top-screen/calendar/last-row.png"
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
const CENTER_X = 63;
|
||||
const CENTER_Y = 95;
|
||||
|
||||
const store = useHomeStore();
|
||||
|
||||
const clockImage = useTemplateRef("clockImage");
|
||||
|
||||
function drawLine(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x0: number,
|
||||
@@ -48,6 +52,16 @@ function drawLine(
|
||||
}
|
||||
|
||||
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 renderHand = (
|
||||
@@ -75,8 +89,8 @@ useRender((ctx) => {
|
||||
ctx.fillStyle = "#494949";
|
||||
ctx.fillRect(CENTER_X - 2, CENTER_Y - 2, 5, 5);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img ref="clockImage" src="/assets/images/home/top-screen/clock.png" hidden />
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
const store = useHomeStore();
|
||||
|
||||
const statusBarImage = useTemplateRef("statusBarImage");
|
||||
const gbaDisplayImage = useTemplateRef("gbaDisplayImage");
|
||||
const startupModeImage = useTemplateRef("startupModeImage");
|
||||
const batteryImage = useTemplateRef("batteryImage");
|
||||
|
||||
// TODO: don't call it here
|
||||
callOnce("intro", () => store.animateIntro());
|
||||
|
||||
useRender((ctx) => {
|
||||
if (!gbaDisplayImage.value || !startupModeImage.value || !batteryImage.value)
|
||||
if (
|
||||
!statusBarImage.value ||
|
||||
!gbaDisplayImage.value ||
|
||||
!startupModeImage.value ||
|
||||
!batteryImage.value
|
||||
)
|
||||
return;
|
||||
|
||||
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.font = "7px NDS7";
|
||||
|
||||
@@ -44,6 +60,11 @@ useRender((ctx) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
ref="statusBarImage"
|
||||
src="/assets/images/home/top-screen/status-bar/status-bar.png"
|
||||
hidden
|
||||
/>
|
||||
<img
|
||||
ref="gbaDisplayImage"
|
||||
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,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user