feat(settings/options/language): intro and outro animation

This commit is contained in:
2026-02-06 13:41:12 +01:00
parent 6bb452a7cf
commit 068d5723bc

View File

@@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import gsap from "gsap";
const { locales, locale, setLocale } = useI18n(); const { locales, locale, setLocale } = useI18n();
const store = useSettingsStore(); const store = useSettingsStore();
const confirmationModal = useConfirmationModal(); const confirmationModal = useConfirmationModal();
@@ -70,7 +72,65 @@ const { selected, selectorPosition } = useButtonNavigation({
}, },
}); });
const handleCancel = () => { const SLIDE_OFFSET = 48;
const SLIDE_DURATION = 0.15;
const OUTRO_OFFSET = 96;
const OUTRO_DURATION = 0.25;
const ROW_STAGGER = SLIDE_DURATION + 0.075;
const ROW_COUNT = 3;
const selectedRow = computed(() => {
const index = BUTTON_KEYS.indexOf(selected.value);
return Math.floor(index / 2);
});
const animation = reactive({
rowOffsetY: Array(ROW_COUNT).fill(SLIDE_OFFSET) as number[],
rowOpacity: Array(ROW_COUNT).fill(0) as number[],
});
const animateIntro = async () => {
const timeline = gsap.timeline();
for (let i = 0; i < ROW_COUNT; i++) {
timeline
.to(
animation.rowOffsetY,
{ [i]: 0, duration: SLIDE_DURATION, ease: "none" },
i * ROW_STAGGER,
)
.to(
animation.rowOpacity,
{ [i]: 1, duration: SLIDE_DURATION, ease: "none" },
i * ROW_STAGGER,
);
}
await timeline;
};
const animateOutro = async () => {
const timeline = gsap.timeline();
for (let i = 0; i < ROW_COUNT; i++) {
timeline
.to(
animation.rowOffsetY,
{ [i]: OUTRO_OFFSET, duration: OUTRO_DURATION, ease: "none" },
0,
)
.to(
animation.rowOpacity,
{ [i]: 0, duration: OUTRO_DURATION, ease: "none" },
0,
);
}
await timeline;
};
onMounted(() => {
animateIntro();
});
const handleCancel = async () => {
await animateOutro();
store.closeSubMenu(); store.closeSubMenu();
}; };
@@ -92,7 +152,10 @@ const handleConfirm = () => {
{}, {},
{ locale: selectedLocale.code }, { locale: selectedLocale.code },
), ),
onClosed: () => store.closeSubMenu(), onClosed: async () => {
await animateOutro();
store.closeSubMenu();
},
timeout: 2000, timeout: 2000,
}); });
}; };
@@ -102,18 +165,25 @@ onRender((ctx) => {
ctx.fillStyle = "#010101"; ctx.fillStyle = "#010101";
for (let i = 0; i < locales.value.length; i += 1) { for (let i = 0; i < locales.value.length; i += 1) {
const row = Math.floor(i / 2);
const [x, y] = BUTTON_POSITIONS[i]!; const [x, y] = BUTTON_POSITIONS[i]!;
const isSelected = selected.value === BUTTON_KEYS[i]; const isSelected = selected.value === BUTTON_KEYS[i];
const buttonImage = isSelected const buttonImage = isSelected
? assets.images.settings.bottomScreen.options.languageButtonActive ? assets.images.settings.bottomScreen.options.languageButtonActive
: assets.images.settings.bottomScreen.options.languageButton; : assets.images.settings.bottomScreen.options.languageButton;
ctx.save();
ctx.globalAlpha = animation.rowOpacity[row]!;
ctx.translate(0, animation.rowOffsetY[row]!);
buttonImage.draw(ctx, x, y, isSelected ? { colored: true } : undefined); buttonImage.draw(ctx, x, y, isSelected ? { colored: true } : undefined);
const name = locales.value[i]?.name; const name = locales.value[i]?.name;
if (!name) { if (!name) {
throw new Error(`Missing locale or locale name: ${BUTTON_KEYS[i]}`); throw new Error(`Missing locale or locale name: ${BUTTON_KEYS[i]}`);
} }
fillTextHCentered(ctx, name, x, y + 20, 96); fillTextHCentered(ctx, name, x, y + 20, 96);
ctx.restore();
} }
}); });
@@ -131,5 +201,13 @@ defineOptions({
@activate-a="handleConfirm" @activate-a="handleConfirm"
/> />
<CommonButtonSelector :rect="selectorPosition" /> <CommonButtonSelector
:rect="[
selectorPosition[0],
selectorPosition[1] + animation.rowOffsetY[selectedRow]!,
selectorPosition[2],
selectorPosition[3],
]"
:opacity="animation.rowOpacity[selectedRow]!"
/>
</template> </template>