From 0093c00ff940142860108be8d35a87f6c9e0c93d Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Sun, 8 Feb 2026 20:19:40 +0100 Subject: [PATCH] feat(settings): block interactions while animation is happening --- .../Settings/BottomScreen/Menus/Clock/Achievements.vue | 10 ++++++++++ .../Settings/BottomScreen/Menus/Clock/Date.vue | 7 +++++++ .../Settings/BottomScreen/Menus/Clock/Time.vue | 7 +++++++ .../Settings/BottomScreen/Menus/Options/2048.vue | 8 ++++++++ .../Settings/BottomScreen/Menus/Options/Language.vue | 8 ++++++++ .../BottomScreen/Menus/Options/RenderingMode.vue | 8 ++++++++ .../Settings/BottomScreen/Menus/TouchScreen/TapTap.vue | 7 +++++++ .../Settings/BottomScreen/Menus/User/Birthday.vue | 7 +++++++ .../Settings/BottomScreen/Menus/User/Snake.vue | 7 +++++++ .../Settings/BottomScreen/Menus/User/UserName.vue | 7 +++++++ app/components/Settings/BottomScreen/NumberInput.vue | 9 +++++++-- 11 files changed, 83 insertions(+), 2 deletions(-) diff --git a/app/components/Settings/BottomScreen/Menus/Clock/Achievements.vue b/app/components/Settings/BottomScreen/Menus/Clock/Achievements.vue index 33f8f13..ce7a76b 100644 --- a/app/components/Settings/BottomScreen/Menus/Clock/Achievements.vue +++ b/app/components/Settings/BottomScreen/Menus/Clock/Achievements.vue @@ -33,6 +33,8 @@ const achievements = useAchievementsStore(); const achievementsScreen = useAchievementsScreen(); const confirmationModal = useConfirmationModal(); +const isAnimating = ref(true); + const SLIDE_OFFSET = 96; const SLIDE_DURATION = 0.25; const ARROW_SLIDE_DELAY = 0.15; @@ -50,6 +52,7 @@ const obtainedRef = const totalRef = useTemplateRef>("total"); const animateIntro = async () => { + isAnimating.value = true; await Promise.all([ obtainedRef.value?.animateIntro(), totalRef.value?.animateIntro(), @@ -63,9 +66,11 @@ const animateIntro = async () => { SLIDE_DURATION + ARROW_SLIDE_DELAY, ), ]); + isAnimating.value = false; }; const animateOutro = async () => { + isAnimating.value = true; await Promise.all([ obtainedRef.value?.animateOutro(), totalRef.value?.animateOutro(), @@ -94,11 +99,13 @@ onMounted(() => { }); const handleCancel = async () => { + if (isAnimating.value) return; await animateOutro(); store.closeSubMenu(); }; const handleReset = () => { + if (isAnimating.value) return; confirmationModal.open({ text: $t("settings.clock.achievements.resetConfirmation"), onConfirm: () => { @@ -108,10 +115,12 @@ const handleReset = () => { }; const handleVisitAll = () => { + if (isAnimating.value) return; achievementsScreen.animateFadeToBlackIntro(); }; onClick((x, y) => { + if (isAnimating.value) return; const viewAllRect = achievementAssets.viewAllButton.rect; if (rectContains([127, 2, viewAllRect.width, viewAllRect.height], [x, y])) { handleVisitAll(); @@ -119,6 +128,7 @@ onClick((x, y) => { }); useKeyDown((key) => { + if (isAnimating.value) return; if (key === "NDS_X") { handleVisitAll(); } diff --git a/app/components/Settings/BottomScreen/Menus/Clock/Date.vue b/app/components/Settings/BottomScreen/Menus/Clock/Date.vue index ca7cb41..0f7ade2 100644 --- a/app/components/Settings/BottomScreen/Menus/Clock/Date.vue +++ b/app/components/Settings/BottomScreen/Menus/Clock/Date.vue @@ -12,19 +12,24 @@ useIntervalFn(() => { now.value = new Date(); }, 1000); +const isAnimating = ref(true); + const monthRef = useTemplateRef>("month"); const dayRef = useTemplateRef>("day"); const yearRef = useTemplateRef>("year"); const animateIntro = async () => { + isAnimating.value = true; await Promise.all([ monthRef.value?.animateIntro(), dayRef.value?.animateIntro(), yearRef.value?.animateIntro(), ]); + isAnimating.value = false; }; const animateOutro = async () => { + isAnimating.value = true; await Promise.all([ monthRef.value?.animateOutro(), dayRef.value?.animateOutro(), @@ -37,11 +42,13 @@ onMounted(() => { }); const handleCancel = async () => { + if (isAnimating.value) return; await animateOutro(); store.closeSubMenu(); }; const handleConfirm = async () => { + if (isAnimating.value) return; await animateOutro(); store.closeSubMenu(); }; diff --git a/app/components/Settings/BottomScreen/Menus/Clock/Time.vue b/app/components/Settings/BottomScreen/Menus/Clock/Time.vue index d2ba3dc..2882711 100644 --- a/app/components/Settings/BottomScreen/Menus/Clock/Time.vue +++ b/app/components/Settings/BottomScreen/Menus/Clock/Time.vue @@ -21,10 +21,13 @@ const animation = reactive({ opacity: 0, }); +const isAnimating = ref(true); + const hourRef = useTemplateRef>("hour"); const minuteRef = useTemplateRef>("minute"); const animateIntro = async () => { + isAnimating.value = true; await Promise.all([ hourRef.value?.animateIntro(), minuteRef.value?.animateIntro(), @@ -33,9 +36,11 @@ const animateIntro = async () => { .to(animation, { offsetY: 0, duration: SLIDE_DURATION, ease: "none" }, 0) .to(animation, { opacity: 1, duration: SLIDE_DURATION, ease: "none" }, 0), ]); + isAnimating.value = false; }; const animateOutro = async () => { + isAnimating.value = true; await Promise.all([ hourRef.value?.animateOutro(), minuteRef.value?.animateOutro(), @@ -55,11 +60,13 @@ onMounted(() => { }); const handleCancel = async () => { + if (isAnimating.value) return; await animateOutro(); store.closeSubMenu(); }; const handleConfirm = async () => { + if (isAnimating.value) return; await animateOutro(); store.closeSubMenu(); }; diff --git a/app/components/Settings/BottomScreen/Menus/Options/2048.vue b/app/components/Settings/BottomScreen/Menus/Options/2048.vue index 61288f6..4b393b5 100644 --- a/app/components/Settings/BottomScreen/Menus/Options/2048.vue +++ b/app/components/Settings/BottomScreen/Menus/Options/2048.vue @@ -9,6 +9,7 @@ const { assets } = useAssets(); const { onRender } = useScreen(); const handleActivateB = () => { + if (isAnimating.value) return; confirmationModal.open({ text: $t("settings.options.2048.quitConfirmation"), onConfirm: () => {}, @@ -22,6 +23,7 @@ const handleActivateB = () => { }; const handleActivateA = () => { + if (isAnimating.value) return; if (isDead()) { resetBoard(); return; @@ -70,6 +72,8 @@ const SLIDE_DURATION = 0.25; const SCORE_OFFSET = -20; const SCORE_DURATION = 0.15; +const isAnimating = ref(true); + const intro = reactive({ frameOffsetY: SLIDE_OFFSET, frameOpacity: 0, @@ -137,6 +141,7 @@ const animateSpawnAll = () => { }; const animateIntro = async () => { + isAnimating.value = true; buildTilesFromBoard(); await gsap @@ -156,9 +161,11 @@ const animateIntro = async () => { { scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" }, SLIDE_DURATION, ); + isAnimating.value = false; }; const animateOutro = async () => { + isAnimating.value = true; await gsap .timeline() .to( @@ -530,6 +537,7 @@ const slide = (rowDir: number, colDir: number) => { }; useKeyDown((key) => { + if (isAnimating.value) return; switch (key) { // TODO: remove this, testing only case "n": diff --git a/app/components/Settings/BottomScreen/Menus/Options/Language.vue b/app/components/Settings/BottomScreen/Menus/Options/Language.vue index 56431b5..94ea012 100644 --- a/app/components/Settings/BottomScreen/Menus/Options/Language.vue +++ b/app/components/Settings/BottomScreen/Menus/Options/Language.vue @@ -26,6 +26,8 @@ const BUTTON_POSITIONS = [ [143, 128], ] as const; +const isAnimating = ref(true); + const { selected, selectorPosition } = useButtonNavigation({ buttons: { english: [10, 27, 106, 41], @@ -66,6 +68,7 @@ const { selected, selectorPosition } = useButtonNavigation({ left: "italian", }, }, + disabled: isAnimating, selectorAnimation: { duration: 0.1, ease: "power2.out", @@ -90,6 +93,7 @@ const animation = reactive({ }); const animateIntro = async () => { + isAnimating.value = true; const timeline = gsap.timeline(); for (let i = 0; i < ROW_COUNT; i++) { timeline @@ -105,9 +109,11 @@ const animateIntro = async () => { ); } await timeline; + isAnimating.value = false; }; const animateOutro = async () => { + isAnimating.value = true; const timeline = gsap.timeline(); for (let i = 0; i < ROW_COUNT; i++) { timeline @@ -130,11 +136,13 @@ onMounted(() => { }); const handleCancel = async () => { + if (isAnimating.value) return; await animateOutro(); store.closeSubMenu(); }; const handleConfirm = () => { + if (isAnimating.value) return; const selectedLocale = locales.value[BUTTON_KEYS.indexOf(selected.value)]!; setLocale(selectedLocale.code); diff --git a/app/components/Settings/BottomScreen/Menus/Options/RenderingMode.vue b/app/components/Settings/BottomScreen/Menus/Options/RenderingMode.vue index 78744db..d2557f7 100644 --- a/app/components/Settings/BottomScreen/Menus/Options/RenderingMode.vue +++ b/app/components/Settings/BottomScreen/Menus/Options/RenderingMode.vue @@ -16,6 +16,8 @@ const BUTTON_STAGGER = 0.3; const SLIDE_OFFSET = 96; const SLIDE_DURATION = 0.25; +const isAnimating = ref(true); + const animation = reactive({ _3dMode: { headerOffsetY: HEADER_HEIGHT * 3, opacity: 0 }, _2dMode: { headerOffsetY: HEADER_HEIGHT * 3, opacity: 0 }, @@ -24,6 +26,7 @@ const animation = reactive({ }); const animateIntro = async () => { + isAnimating.value = true; await gsap .timeline() .to( @@ -46,9 +49,11 @@ const animateIntro = async () => { }, BUTTON_STAGGER, ); + isAnimating.value = false; }; const animateOutro = async () => { + isAnimating.value = true; await gsap .timeline() .to( @@ -77,6 +82,7 @@ const { selected, selectorPosition } = useButtonNavigation({ _3dMode: { down: "_2dMode" }, _2dMode: { up: "_3dMode" }, }, + disabled: isAnimating, selectorAnimation: { ease: "none", duration: 0.065, @@ -84,11 +90,13 @@ const { selected, selectorPosition } = useButtonNavigation({ }); const handleCancel = async () => { + if (isAnimating.value) return; await animateOutro(); store.closeSubMenu(); }; const handleConfirm = () => { + if (isAnimating.value) return; const mode = selected.value === "_3dMode" ? "3d" : "2d"; app.setRenderingMode(mode); diff --git a/app/components/Settings/BottomScreen/Menus/TouchScreen/TapTap.vue b/app/components/Settings/BottomScreen/Menus/TouchScreen/TapTap.vue index 861808e..6c22077 100644 --- a/app/components/Settings/BottomScreen/Menus/TouchScreen/TapTap.vue +++ b/app/components/Settings/BottomScreen/Menus/TouchScreen/TapTap.vue @@ -66,6 +66,8 @@ const highScore = useLocalStorage("taptap_high_score", 0); let score = 0; let isNewBest = false; +const isAnimating = ref(true); + const AREA_FADE_DURATION = 0.2; const SCORE_OFFSET = -20; const SCORE_DURATION = 0.15; @@ -77,6 +79,7 @@ const animation = reactive({ }); const animateIntro = async () => { + isAnimating.value = true; await gsap .timeline() .to( @@ -89,9 +92,11 @@ const animateIntro = async () => { { scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" }, AREA_FADE_DURATION, ); + isAnimating.value = false; }; const animateOutro = async () => { + isAnimating.value = true; targetX = 0; targetY = LOGICAL_HEIGHT * 2 - 20; @@ -119,6 +124,7 @@ onMounted(() => { }); const handleActivateB = () => { + if (isAnimating.value) return; if (state.value === "playing") { state.value = "paused"; confirmationModal.open({ @@ -140,6 +146,7 @@ const handleActivateB = () => { }; const handleActivateA = async () => { + if (isAnimating.value) return; if (state.value === "playing") { state.value = "paused"; confirmationModal.open({ diff --git a/app/components/Settings/BottomScreen/Menus/User/Birthday.vue b/app/components/Settings/BottomScreen/Menus/User/Birthday.vue index 5684d08..91e1bd0 100644 --- a/app/components/Settings/BottomScreen/Menus/User/Birthday.vue +++ b/app/components/Settings/BottomScreen/Menus/User/Birthday.vue @@ -8,19 +8,24 @@ const BIRTHDAY_DAY = 25; const BIRTHDAY_MONTH = 4; const BIRTHDAY_YEAR = 2002; +const isAnimating = ref(true); + const monthRef = useTemplateRef>("month"); const dayRef = useTemplateRef>("day"); const yearRef = useTemplateRef>("year"); const animateIntro = async () => { + isAnimating.value = true; await Promise.all([ monthRef.value?.animateIntro(), dayRef.value?.animateIntro(), yearRef.value?.animateIntro(), ]); + isAnimating.value = false; }; const animateOutro = async () => { + isAnimating.value = true; await Promise.all([ monthRef.value?.animateOutro(), dayRef.value?.animateOutro(), @@ -33,11 +38,13 @@ onMounted(() => { }); const handleActivateB = async () => { + if (isAnimating.value) return; await animateOutro(); store.closeSubMenu(); }; const handleActivateA = () => { + if (isAnimating.value) return; const today = new Date(); const currentYear = today.getFullYear(); diff --git a/app/components/Settings/BottomScreen/Menus/User/Snake.vue b/app/components/Settings/BottomScreen/Menus/User/Snake.vue index e98d405..1c34b60 100644 --- a/app/components/Settings/BottomScreen/Menus/User/Snake.vue +++ b/app/components/Settings/BottomScreen/Menus/User/Snake.vue @@ -19,6 +19,8 @@ const TEXT_FADE_DURATION = 0.15; const SCORE_OFFSET = -20; const SCORE_DURATION = 0.15; +const isAnimating = ref(true); + const intro = reactive({ boardOffsetY: BOARD_SLIDE_OFFSET, boardOpacity: 0, @@ -27,6 +29,7 @@ const intro = reactive({ }); const animateIntro = async () => { + isAnimating.value = true; await gsap .timeline() .to( @@ -49,9 +52,11 @@ const animateIntro = async () => { { scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" }, BOARD_SLIDE_DURATION + TEXT_FADE_DURATION, ); + isAnimating.value = false; }; const animateOutro = async () => { + isAnimating.value = true; await gsap .timeline() .to( @@ -80,6 +85,7 @@ onMounted(() => { }); const handleCancel = async () => { + if (isAnimating.value) return; switch (state.value) { case "alive": { state.value = "pause"; @@ -109,6 +115,7 @@ const handleCancel = async () => { }; const handleConfirm = () => { + if (isAnimating.value) return; switch (state.value) { case "alive": { state.value = "pause"; diff --git a/app/components/Settings/BottomScreen/Menus/User/UserName.vue b/app/components/Settings/BottomScreen/Menus/User/UserName.vue index dc7d18a..0317d44 100644 --- a/app/components/Settings/BottomScreen/Menus/User/UserName.vue +++ b/app/components/Settings/BottomScreen/Menus/User/UserName.vue @@ -17,12 +17,15 @@ const OUTRO_DURATION = 0.25; const ROW_STAGGER = SLIDE_DURATION + 0.075; const ROW_COUNT = 2; +const isAnimating = ref(true); + const animation = reactive({ rowOffsetY: new Array(ROW_COUNT).fill(SLIDE_OFFSET), rowOpacity: new Array(ROW_COUNT).fill(0), }); const animateIntro = async () => { + isAnimating.value = true; const timeline = gsap.timeline(); for (let i = 0; i < ROW_COUNT; i++) { timeline @@ -38,9 +41,11 @@ const animateIntro = async () => { ); } await timeline; + isAnimating.value = false; }; const animateOutro = async () => { + isAnimating.value = true; const timeline = gsap.timeline(); for (let i = 0; i < ROW_COUNT; i++) { timeline @@ -63,11 +68,13 @@ onMounted(() => { }); const handleCancel = async () => { + if (isAnimating.value) return; await animateOutro(); store.closeSubMenu(); }; const handleConfirm = async () => { + if (isAnimating.value) return; await animateOutro(); store.closeSubMenu(); }; diff --git a/app/components/Settings/BottomScreen/NumberInput.vue b/app/components/Settings/BottomScreen/NumberInput.vue index 288d7aa..f4ec9ce 100644 --- a/app/components/Settings/BottomScreen/NumberInput.vue +++ b/app/components/Settings/BottomScreen/NumberInput.vue @@ -85,6 +85,8 @@ const SLIDE_DURATION = 0.25; const ARROW_SLIDE_DELAY = 0.15; const ARROW_SLIDE_DURATION = 0.15; +const isAnimating = ref(true); + const animation = reactive({ offsetY: SLIDE_OFFSET, opacity: 0, @@ -93,6 +95,7 @@ const animation = reactive({ }); const animateIntro = async () => { + isAnimating.value = true; await gsap .timeline() .to(animation, { offsetY: 0, duration: SLIDE_DURATION, ease: "none" }, 0) @@ -107,9 +110,11 @@ const animateIntro = async () => { { downArrowOffsetY: 0, duration: ARROW_SLIDE_DURATION, ease: "none" }, SLIDE_DURATION + ARROW_SLIDE_DELAY, ); + isAnimating.value = false; }; const animateOutro = async () => { + isAnimating.value = true; await gsap .timeline() .to( @@ -208,7 +213,7 @@ onRender((ctx) => { }, 10); useKeyDown((key) => { - if (!props.selected || props.disabled) return; + if (isAnimating.value || !props.selected || props.disabled) return; switch (key) { case "NDS_UP": increase(); @@ -220,7 +225,7 @@ useKeyDown((key) => { }); onClick((x, y) => { - if (props.disabled) return; + if (isAnimating.value || props.disabled) return; if ( rectContains( [props.x, Y, upImage.value.rect.width, ARROW_IMAGE_HEIGHT],