feat(settings): intro and outro animation

This commit is contained in:
2026-02-05 20:44:19 +01:00
parent 098285ee82
commit 1236e86981
10 changed files with 306 additions and 52 deletions

View File

@@ -24,7 +24,7 @@ onRender((ctx) => {
const daysInMonth = new Date(year, month + 1, 0).getDate(); const daysInMonth = new Date(year, month + 1, 0).getDate();
ctx.globalAlpha = store.isIntro ctx.globalAlpha = store.isIntro
? store.intro.stage1Opacity ? store.intro.topScreenOpacity
: store.isOutro && store.outro.animateTop : store.isOutro && store.outro.animateTop
? store.outro.stage1Opacity ? store.outro.stage1Opacity
: 1; : 1;
@@ -44,7 +44,7 @@ onRender((ctx) => {
} }
ctx.globalAlpha = store.isIntro ctx.globalAlpha = store.isIntro
? store.intro.stage1Opacity ? store.intro.topScreenOpacity
: store.isOutro && store.outro.animateTop : store.isOutro && store.outro.animateTop
? store.outro.stage2Opacity ? store.outro.stage2Opacity
: 1; : 1;

View File

@@ -55,14 +55,14 @@ function drawLine(
onRender((ctx) => { onRender((ctx) => {
ctx.globalAlpha = store.isIntro ctx.globalAlpha = store.isIntro
? store.intro.stage1Opacity ? store.intro.topScreenOpacity
: store.isOutro && store.outro.animateTop : store.isOutro && store.outro.animateTop
? store.outro.stage1Opacity ? store.outro.stage1Opacity
: 1; : 1;
assets.images.home.topScreen.clock.draw(ctx, 13, 45); assets.images.home.topScreen.clock.draw(ctx, 13, 45);
ctx.globalAlpha = store.isIntro ctx.globalAlpha = store.isIntro
? store.intro.stage1Opacity ? store.intro.topScreenOpacity
: store.isOutro && store.outro.animateTop : store.isOutro && store.outro.animateTop
? store.outro.stage2Opacity ? store.outro.stage2Opacity
: 1; : 1;

View File

@@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import Background from "./Background.vue"; import Background from "./Background.vue";
import Menus from "./Menus/Menus.vue"; import Menus from "./Menus/Menus.vue";
const store = useSettingsStore();
</script> </script>
<template> <template>
<Background /> <Background />
<CommonBars /> <CommonBars :y-offset="store.barOffsetY" />
<Menus /> <Menus />
<CommonConfirmationModal /> <CommonConfirmationModal />
<AchievementsFadeToBlack /> <AchievementsFadeToBlack />

View File

@@ -21,6 +21,10 @@ import Selector from "~/components/Common/ButtonSelector.vue";
const app = useAppStore(); const app = useAppStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
if (app.previousScreen === "home") {
settingsStore.selectedButton = "options";
}
const isMainMenu = (button: string): button is SettingsMenu => const isMainMenu = (button: string): button is SettingsMenu =>
SETTINGS_MENUS.includes(button as SettingsMenu); SETTINGS_MENUS.includes(button as SettingsMenu);
@@ -195,30 +199,78 @@ const viewComponents: Record<string, Component> = {
touchScreenTapTap: TouchScreenTapTap, touchScreenTapTap: TouchScreenTapTap,
}; };
const selectorXOffset = computed(() => {
const menu = isMainMenu(selected.value)
? selected.value
: getParentMenu(selected.value);
switch (menu) {
case "clock":
return Math.min(0, settingsStore.menuOffsets[1]);
case "user":
return Math.min(
0,
settingsStore.menuOffsets[1] + settingsStore.menuOffsets[2],
);
case "touchScreen":
return Math.min(
0,
settingsStore.menuOffsets[1] +
settingsStore.menuOffsets[2] +
settingsStore.menuOffsets[3],
);
default:
return 0;
}
});
</script> </script>
<template> <template>
<template v-if="!settingsStore.currentSubMenu"> <template v-if="!settingsStore.currentSubMenu">
<OptionsMenu :x="33" :y="121" /> <TouchScreenMenu
<ClockMenu :x="81" :y="121" /> :x="
<UserMenu :x="129" :y="121" /> 177 +
<TouchScreenMenu :x="177" :y="121" :opacity="1" /> settingsStore.menuOffsets[1] +
settingsStore.menuOffsets[2] +
settingsStore.menuOffsets[3]
"
:y="121 + settingsStore.menuYOffset"
:opacity="1"
/>
<UserMenu
:x="129 + settingsStore.menuOffsets[1] + settingsStore.menuOffsets[2]"
:y="121 + settingsStore.menuYOffset"
/>
<ClockMenu
:x="81 + settingsStore.menuOffsets[1]"
:y="121 + settingsStore.menuYOffset"
/>
<OptionsMenu :x="33" :y="121 + settingsStore.menuYOffset" />
<Selector :rect="selectorPosition" :opacity="1" /> <Selector
:rect="[
selectorPosition[0] + selectorXOffset,
selectorPosition[1] + settingsStore.menuYOffset,
selectorPosition[2],
selectorPosition[3],
]"
:opacity="1"
/>
<CommonButtons <CommonButtons
v-if="isSubmenuSelected" v-if="isSubmenuSelected"
:y-offset="0" :y-offset="settingsStore.barOffsetY"
:b-label="$t('common.goBack')" :b-label="$t('common.goBack')"
:a-label="$t('common.select')" :a-label="$t('common.select')"
@activate-b="select(getParentMenu(selected))" @activate-b="select(getParentMenu(selected))"
/> />
<CommonButtons <CommonButtons
v-else v-else
:y-offset="0" :y-offset="settingsStore.barOffsetY"
:b-label="$t('common.quit')" :b-label="$t('common.quit')"
:a-label="$t('common.select')" :a-label="$t('common.select')"
@activate-b="app.navigateTo('home')" @activate-b="settingsStore.animateOutro()"
/> />
</template> </template>
<component :is="viewComponents[settingsStore.currentSubMenu]" v-else /> <component :is="viewComponents[settingsStore.currentSubMenu]" v-else />

View File

@@ -1,13 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
const { onRender } = useScreen(); const { onRender } = useScreen();
const store = useSettingsStore();
const { assets } = useAssets(); const { assets } = useAssets();
onRender((ctx) => { onRender((ctx) => {
ctx.fillStyle = "black"; ctx.fillStyle = "black";
ctx.font = "7px NDS7"; ctx.font = "7px NDS7";
ctx.translate(0, -16); ctx.translate(0, -16 + store.notificationYOffset / 3);
const CALENDAR_COLS = 7; const CALENDAR_COLS = 7;
const CALENDAR_ROWS = 5; const CALENDAR_ROWS = 5;

View File

@@ -2,6 +2,7 @@
const { onRender } = useScreen(); const { onRender } = useScreen();
const app = useAppStore(); const app = useAppStore();
const store = useSettingsStore();
const { assets } = useAssets(); const { assets } = useAssets();
const CENTER_X = 63; const CENTER_X = 63;
@@ -53,7 +54,7 @@ function drawLine(
} }
onRender((ctx) => { onRender((ctx) => {
ctx.translate(0, -16); ctx.translate(0, -16 + store.notificationYOffset / 3);
assets.images.home.topScreen.clock.draw(ctx, 13, 45); assets.images.home.topScreen.clock.draw(ctx, 13, 45);

View File

@@ -1,9 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
const { onRender } = useScreen(); import gsap from "gsap";
const { onRender } = useScreen();
const store = useSettingsStore(); const store = useSettingsStore();
const { assets } = useAssets(); const { assets } = useAssets();
type NotificationData = {
image: AtlasImage;
title: string;
description: string;
};
const menuYOffset = ref(0);
const submenuYOffset = ref(0);
const visibleMenuNotification = ref<NotificationData | null>(null);
const visibleSubmenuNotification = ref<NotificationData | null>(null);
const renderNotification = ( const renderNotification = (
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
image: AtlasImage, image: AtlasImage,
@@ -83,13 +95,62 @@ const submenuNotification = computed(() => {
}; };
}); });
const animateNotification = (
curr: NotificationData | null,
prev: NotificationData | null,
visibleRef: Ref<NotificationData | null>,
offsetRef: Ref<number>,
) => {
if (prev !== null && curr === null) {
// slide down
visibleRef.value = prev;
gsap.fromTo(
offsetRef,
{ value: 0 },
{
value: 48,
duration: 0.2,
ease: "none",
onComplete: () => {
visibleRef.value = null;
},
},
);
} else if (prev === null && curr !== null) {
// slide up
visibleRef.value = curr;
gsap.fromTo(
offsetRef,
{ value: 48 },
{ value: 0, duration: 0.2, ease: "none" },
);
}
};
watch(menuNotification, (curr, prev) => {
animateNotification(curr, prev, visibleMenuNotification, menuYOffset);
});
watch(submenuNotification, (curr, prev) => {
animateNotification(curr, prev, visibleSubmenuNotification, submenuYOffset);
});
onRender((ctx) => { onRender((ctx) => {
let count = 1; const submenuY = 144 + submenuYOffset.value;
if (menuNotification.value) count++; const menuY = visibleSubmenuNotification.value
if (submenuNotification.value) count++; ? Math.min(144, submenuY - 16) + menuYOffset.value
: 144 + menuYOffset.value;
ctx.translate(0, 144 - (count - 1) * 16); const nextY = visibleMenuNotification.value
? menuY
: visibleSubmenuNotification.value
? submenuY
: null;
const mainY =
nextY !== null
? Math.min(144, nextY - 16) + store.notificationYOffset
: 144 + store.notificationYOffset;
ctx.translate(0, mainY);
renderNotification( renderNotification(
ctx, ctx,
mainNotification.value.image, mainNotification.value.image,
@@ -97,23 +158,24 @@ onRender((ctx) => {
mainNotification.value.description, mainNotification.value.description,
); );
if (menuNotification.value) { if (visibleMenuNotification.value) {
ctx.translate(0, 16); ctx.translate(0, menuY - mainY);
renderNotification( renderNotification(
ctx, ctx,
menuNotification.value.image, visibleMenuNotification.value.image,
menuNotification.value.title, visibleMenuNotification.value.title,
menuNotification.value.description, visibleMenuNotification.value.description,
); );
} }
if (submenuNotification.value) { if (visibleSubmenuNotification.value) {
ctx.translate(0, 16); const prevY = visibleMenuNotification.value ? menuY : mainY;
ctx.translate(0, submenuY - prevY);
renderNotification( renderNotification(
ctx, ctx,
submenuNotification.value.image, visibleSubmenuNotification.value.image,
submenuNotification.value.title, visibleSubmenuNotification.value.title,
submenuNotification.value.description, visibleSubmenuNotification.value.description,
); );
} }
}); });

View File

@@ -4,6 +4,12 @@ import Calendar from "./Calendar.vue";
import Clock from "./Clock.vue"; import Clock from "./Clock.vue";
import StatusBar from "./StatusBar.vue"; import StatusBar from "./StatusBar.vue";
import Notifications from "./Notifications.vue"; import Notifications from "./Notifications.vue";
const store = useSettingsStore();
onMounted(() => {
store.animateIntro();
});
</script> </script>
<template> <template>

View File

@@ -13,6 +13,7 @@ export const useHomeStore = defineStore("home", {
intro: { intro: {
statusBarY: -20, statusBarY: -20,
stage1Opacity: 0, stage1Opacity: 0,
topScreenOpacity: 0,
}, },
outro: { outro: {
@@ -44,33 +45,51 @@ export const useHomeStore = defineStore("home", {
animateIntro() { animateIntro() {
this.isIntro = true; this.isIntro = true;
const app = useAppStore();
const timeline = gsap.timeline({ const timeline = gsap.timeline({
onComplete: () => { onComplete: () => {
this.isIntro = false; this.isIntro = false;
}, },
}); });
timeline timeline.fromTo(
.fromTo( this.intro,
this.intro, { stage1Opacity: 0 },
{ stage1Opacity: 0 }, {
{ stage1Opacity: 1,
stage1Opacity: 1, duration: 0.5,
duration: 0.5, ease: "none",
ease: "none", },
}, 0,
0, );
)
.fromTo( if (app.previousScreen !== "settings") {
this.intro, timeline
{ statusBarY: -20 }, .fromTo(
{ this.intro,
statusBarY: 0, { topScreenOpacity: 0 },
duration: 0.15, {
ease: "none", topScreenOpacity: 1,
}, duration: 0.5,
0.35, ease: "none",
); },
0,
)
.fromTo(
this.intro,
{ statusBarY: -20 },
{
statusBarY: 0,
duration: 0.15,
ease: "none",
},
0.35,
);
} else {
this.intro.topScreenOpacity = 1;
this.intro.statusBarY = 0;
}
}, },
animateOutro(to: AppScreen) { animateOutro(to: AppScreen) {

View File

@@ -1,3 +1,5 @@
import gsap from "gsap";
export const SETTINGS_MENUS = [ export const SETTINGS_MENUS = [
"options", "options",
"clock", "clock",
@@ -31,6 +33,13 @@ export const useSettingsStore = defineStore("settings", {
currentSubMenu: null as SettingsSubMenu | null, currentSubMenu: null as SettingsSubMenu | null,
menuExpanded: false, menuExpanded: false,
selectedButton: "options" as SettingsNavigableButton, selectedButton: "options" as SettingsNavigableButton,
notificationYOffset: 48,
barOffsetY: 24,
menuOffsets: [0, -48, -48, -48] as [number, number, number, number],
menuYOffset: 72,
isIntro: true,
isOutro: false,
}), }),
actions: { actions: {
@@ -58,5 +67,107 @@ export const useSettingsStore = defineStore("settings", {
closeSubMenu() { closeSubMenu() {
this.currentSubMenu = null; this.currentSubMenu = null;
}, },
animateIntro() {
this.isIntro = true;
gsap
.timeline()
// bars
.fromTo(
this,
{ barOffsetY: 24 },
{ barOffsetY: 0, duration: 0.2, ease: "none" },
0,
)
// title notification
.fromTo(
this,
{ notificationYOffset: 48 },
{ notificationYOffset: 0, duration: 0.2, ease: "none" },
0.1,
)
// menus slide up
.fromTo(
this,
{ menuYOffset: 72 },
{ menuYOffset: 0, duration: 0.2, ease: "none" },
0,
)
// menus accordion
.fromTo(
this.menuOffsets,
{ 1: -48 },
{ 1: 0, duration: 0.1, ease: "none" },
0.2,
)
.fromTo(
this.menuOffsets,
{ 2: -48 },
{ 2: 0, duration: 0.1, ease: "none" },
0.3,
)
.fromTo(
this.menuOffsets,
{ 3: -48 },
{ 3: 0, duration: 0.1, ease: "none" },
0.4,
)
.call(() => {
this.isIntro = false;
});
},
animateOutro() {
this.isOutro = true;
gsap
.timeline()
// title notification
.fromTo(
this,
{ notificationYOffset: 0 },
{ notificationYOffset: 48, duration: 0.2, ease: "none" },
0,
)
// bars
.fromTo(
this,
{ barOffsetY: 0 },
{ barOffsetY: 24, duration: 0.2, ease: "none" },
0.1,
)
// menus accordion
.fromTo(
this.menuOffsets,
{ 3: 0 },
{ 3: -48, duration: 0.1, ease: "none" },
0,
)
.fromTo(
this.menuOffsets,
{ 2: 0 },
{ 2: -48, duration: 0.1, ease: "none" },
0.1,
)
.fromTo(
this.menuOffsets,
{ 1: 0 },
{ 1: -48, duration: 0.1, ease: "none" },
0.2,
)
// menus slide down
.fromTo(
this,
{ menuYOffset: 0 },
{ menuYOffset: 72, duration: 0.2, ease: "none" },
0.3,
)
.call(() => {
const app = useAppStore();
app.navigateTo("home");
this.isOutro = false;
});
},
}, },
}); });