feat(nds): add pressed state for all buttons

This commit is contained in:
2026-02-11 23:33:26 +01:00
parent 305ef81083
commit 2137abf424
57 changed files with 413 additions and 369 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
const { onRender, onClick } = useScreen();
const { onRender, onClick, onMouseDown } = useScreen();
const store = useAchievementsScreen();
const achievementsStore = useAchievementsStore();
const { assets } = useAssets();
@@ -8,6 +8,22 @@ const QUIT_SIZE = assets.images.achievements.quit.rect.width;
const QUIT_X = Math.floor(LOGICAL_WIDTH / 2 - QUIT_SIZE / 2);
const QUIT_Y = 135;
const quitPressed = ref(false);
const handleMouseUp = () => {
quitPressed.value = false;
};
onMounted(() => document.addEventListener("mouseup", handleMouseUp));
onUnmounted(() => document.removeEventListener("mouseup", handleMouseUp));
onMouseDown((x, y) => {
if (store.isIntro || store.isOutro) return;
if (rectContains([QUIT_X, QUIT_Y, QUIT_SIZE, QUIT_SIZE], [x, y])) {
quitPressed.value = true;
}
});
useKeyDown(({ key, repeated }) => {
if (store.isIntro || store.isOutro || repeated) return;
@@ -76,7 +92,10 @@ onRender((ctx) => {
: store.isOutro
? store.outro.stage2Opacity
: 1;
assets.images.achievements.quit.draw(ctx, QUIT_X, QUIT_Y);
(quitPressed.value
? assets.images.achievements.quitPressed
: assets.images.achievements.quit
).draw(ctx, QUIT_X, QUIT_Y);
});
defineOptions({

View File

@@ -13,11 +13,10 @@ const emit = defineEmits<{
activateB: [];
}>();
const { onRender, onClick } = useScreen();
const { assets } = useAssets();
const { onRender, onClick, onMouseDown } = useScreen();
const BUTTON_WIDTH = assets.images.common.button.rect.width;
const BUTTON_HEIGHT = assets.images.common.button.rect.height;
const BUTTON_WIDTH = 80;
const BUTTON_HEIGHT = 18;
const B_BUTTON: Rect = [31, 172, BUTTON_WIDTH, BUTTON_HEIGHT];
const A_BUTTON: Rect = [144, 172, BUTTON_WIDTH, BUTTON_HEIGHT];
@@ -31,6 +30,9 @@ let aButtonOffsetY = 0;
let displayedBLabel = props.bLabel;
let displayedALabel = props.aLabel;
let bButtonPressed = false;
let aButtonPressed = false;
const animateLabelChange = (
setter: (v: number) => void,
setLabel: (label: string) => void,
@@ -80,25 +82,24 @@ onRender((ctx) => {
ctx.font = "10px NDS10";
ctx.fillStyle = "#010101";
ctx.translate(0, props.yOffset);
ctx.translate(0, 172 + props.yOffset);
const drawButton = (
icon: string,
text: string,
x: number,
buttonOffsetY: number,
) => {
ctx.save();
ctx.translate(0, buttonOffsetY);
assets.images.common.button.draw(ctx, x, 172);
const label = `${icon} ${text}`;
fillTextHCentered(ctx, label, x, 185, BUTTON_WIDTH);
ctx.restore();
};
drawButton(ICONS.B, displayedBLabel, 31, bButtonOffsetY);
drawButton(ICONS.A, displayedALabel, 144, aButtonOffsetY);
drawButton(
ctx,
`${ICONS.B} ${displayedBLabel}`,
31,
bButtonOffsetY,
BUTTON_WIDTH,
bButtonPressed,
);
drawButton(
ctx,
`${ICONS.A} ${displayedALabel}`,
144,
aButtonOffsetY,
BUTTON_WIDTH,
aButtonPressed,
);
}, 60);
onClick((x, y) => {
@@ -110,6 +111,20 @@ onClick((x, y) => {
}
});
onMouseDown((x, y) => {
if (props.yOffset !== 0) return;
if (rectContains(B_BUTTON, [x, y])) {
bButtonPressed = true;
} else if (rectContains(A_BUTTON, [x, y])) {
aButtonPressed = true;
}
});
useMouseUp(() => {
bButtonPressed = false;
aButtonPressed = false;
});
useKeyDown(({ key, repeated }) => {
if (props.yOffset !== 0 || repeated) return;
switch (key) {

View File

@@ -1,106 +1,14 @@
<script setup lang="ts">
import Background from "./Background.vue";
import Buttons from "./Buttons.vue";
import ButtonSelector from "~/components/Common/ButtonSelector.vue";
import { promiseTimeout } from "@vueuse/core";
const store = useContactStore();
const achievements = useAchievementsStore();
const confirmationModal = useConfirmationModal();
const ACTIONS = {
github: [
"open",
"contact.actions.githubProfile",
"https://github.com/pihkaal",
],
email: ["copy", "contact.actions.email", "hello@pihkaal.me"],
website: ["copy", "contact.actions.websiteLink", "https://pihkaal.me"],
cv: ["open", "contact.actions.cv", "https://pihkaal.me/cv"],
} as const satisfies Record<
string,
[action: "copy" | "open", verbKey: string, content: string]
>;
const { selected, selectorPosition } = useButtonNavigation({
buttons: {
github: [26, 27, 202, 42],
email: [26, 59, 202, 42],
website: [26, 91, 202, 42],
cv: [26, 123, 202, 42],
},
navigation: {
github: {
down: "email",
},
email: {
up: "github",
down: "website",
},
website: {
up: "email",
down: "cv",
},
cv: {
up: "website",
},
},
initialButton: "github",
onActivate: (button) => {
actionateButton(button);
},
disabled: computed(() => store.isIntro || store.isOutro),
selectorAnimation: {
duration: 0.075,
ease: "none",
},
});
const handleActivateB = () => {
if (store.isIntro || store.isOutro) return;
store.animateOutro();
};
const actionateButton = async (button: (typeof selected)["value"]) => {
const [action, verbKey, content] = ACTIONS[button];
const verb = $t(verbKey);
if (action === "copy") {
try {
await navigator.clipboard.writeText(content);
store.pushNotification($t("contact.copiedToClipboard", { item: verb }));
} catch (error) {
console.error("Failed to copy to clipboard:", error);
}
} else {
const url = content.replace(/^https?:\/\//, "");
confirmationModal.open({
text: $t("contact.openUrl", { url }),
onActivateA: async () => {
store.pushNotification($t("contact.opened", { item: verb }));
await promiseTimeout(100);
await navigateTo(content, { open: { target: "_blank " } });
await promiseTimeout(500);
if (button === "github") {
achievements.unlock("contact_git_visit");
}
},
});
}
};
</script>
<template>
<Background />
<Buttons />
<ButtonSelector
:rect="selectorPosition"
:opacity="
store.isIntro ? store.intro.stage3Opacity : store.outro.stage1Opacity
"
/>
<CommonBars
:title="$t('contact.title')"
@@ -109,20 +17,4 @@ const actionateButton = async (button: (typeof selected)["value"]) => {
"
:y-offset="store.isIntro ? store.intro.barOffsetY : 0"
/>
<CommonConfirmationModal />
<CommonButtons
:y-offset="
store.isIntro ? store.intro.barOffsetY : confirmationModal.buttonsYOffset
"
:opacity="
store.isIntro
? store.intro.stage3Opacity
: store.isOutro
? store.outro.stage2Opacity
: 1
"
:b-label="$t('common.quit')"
:a-label="$t(`contact.actions.${ACTIONS[selected][0]}`)"
@activate-b="handleActivateB"
/>
</template>

View File

@@ -1,17 +1,162 @@
<script setup lang="ts">
import ButtonSelector from "~/components/Common/ButtonSelector.vue";
import { promiseTimeout } from "@vueuse/core";
const { onRender } = useScreen();
const store = useContactStore();
const { assets } = useAssets();
const achievements = useAchievementsStore();
const confirmationModal = useConfirmationModal();
const { assets } = useAssets((a) => a.images.contact.bottomScreen.buttons);
const BUTTONS = {
git: {
position: [31, 32],
action: "open",
url: "https://git.pihkaal.xyz",
text: "git.pihkaal.xyz",
},
email: {
position: [31, 64],
action: "copy",
url: "hello@pihkaal.me",
text: "hello@pihkaal.me",
},
linkedin: {
position: [31, 96],
action: "open",
url: "https://linkedin.com/in/stevancorre/",
text: "/in/stevancorre/",
},
cv: {
position: [31, 128],
action: "open",
url: "https://pihkaal.me/cv",
text: "/cv",
},
} as const satisfies Record<
string,
{
position: [number, number];
action: "copy" | "open";
url: string;
text: string;
}
>;
const { selected, pressed, selectorPosition } = useButtonNavigation({
buttons: {
git: [26, 27, 202, 42],
email: [26, 59, 202, 42],
linkedin: [26, 91, 202, 42],
cv: [26, 123, 202, 42],
},
navigation: {
git: {
down: "email",
},
email: {
up: "git",
down: "linkedin",
},
linkedin: {
up: "email",
down: "cv",
},
cv: {
up: "linkedin",
},
},
initialButton: "git",
onActivate: async (button) => {
const { action, url } = BUTTONS[button];
const verb = $t(`contact.actions.${button}`);
if (action === "copy") {
try {
await navigator.clipboard.writeText(url);
store.pushNotification($t("contact.copiedToClipboard", { item: verb }));
} catch (error) {
console.error("Failed to copy to clipboard:", error);
}
} else {
confirmationModal.open({
text: $t("contact.openUrl", { url: url.replace(/^https?:\/\//, "") }),
onActivateA: async () => {
store.pushNotification($t("contact.opened", { item: verb }));
await promiseTimeout(100);
await navigateTo(url, { open: { target: "_blank " } });
await promiseTimeout(500);
if (button === "git") {
achievements.unlock("contact_git_visit");
}
},
});
}
},
disabled: computed(() => store.isIntro || store.isOutro),
selectorAnimation: {
duration: 0.075,
ease: "none",
},
});
const handleActivateB = () => {
if (store.isIntro || store.isOutro) return;
store.animateOutro();
};
onRender((ctx) => {
ctx.globalAlpha = store.isIntro
? store.intro.stage3Opacity
: store.outro.stage1Opacity;
assets.images.contact.bottomScreen.buttons.draw(ctx, 31, 32);
});
defineOptions({
render: () => null,
ctx.textBaseline = "top";
ctx.font = "10px NDS10";
ctx.fillStyle = "#515151";
for (const [
buttonName,
{
text,
position: [x, y],
},
] of Object.entries(BUTTONS)) {
const button = buttonName as typeof selected.value;
const isPressed = pressed.value === button;
assets[isPressed ? "buttonPressed" : "button"].draw(ctx, x, y);
const pressedKey: `${typeof selected.value}Pressed` = `${button}Pressed`;
assets[isPressed ? pressedKey : button].draw(ctx, x + 1, y + 1);
ctx.fillText(text, x + 36, y + 11);
}
});
</script>
<template>
<ButtonSelector
:rect="selectorPosition"
:opacity="
store.isIntro ? store.intro.stage3Opacity : store.outro.stage1Opacity
"
/>
<CommonConfirmationModal />
<CommonButtons
:y-offset="
store.isIntro ? store.intro.barOffsetY : confirmationModal.buttonsYOffset
"
:opacity="
store.isIntro
? store.intro.stage3Opacity
: store.isOutro
? store.outro.stage2Opacity
: 1
"
:b-label="$t('common.quit')"
:a-label="$t(`contact.actions.${BUTTONS[selected].action}`)"
@activate-b="handleActivateB"
/>
</template>

View File

@@ -5,9 +5,9 @@ import Selector from "~/components/Common/ButtonSelector.vue";
const { onRender } = useScreen();
const store = useHomeStore();
const { assets } = useAssets();
const { assets } = useAssets((a) => a.images.home.bottomScreen.buttons);
const { selected, selectorPosition } = useButtonNavigation({
const { selected, pressed, selectorPosition } = useButtonNavigation({
buttons: {
projects: [31, 23, 193, 49],
contact: [31, 71, 97, 49],
@@ -20,12 +20,6 @@ const { selected, selectorPosition } = useButtonNavigation({
initialButton: store.selectedButton,
onActivate: (button) => {
if (button === "theme") throw new Error(`Not implemented: ${button}`);
if (button === "achievements") {
store.animateOutro("achievements");
return;
}
store.animateOutro(button);
},
navigation: {
@@ -68,6 +62,14 @@ watch(selected, (newSelected) => {
store.selectedButton = newSelected;
});
const getButtonImage = (button: typeof selected.value) => {
if (pressed.value === button) {
const pressedKey: `${typeof selected.value}Pressed` = `${button}Pressed`;
if (pressedKey in assets) return assets[pressedKey];
}
return assets[button];
};
const getButtonOffset = (button: (typeof selected)["value"]) => {
if (selected.value === button) return store.outro.buttonOffsetY;
return 0;
@@ -85,7 +87,7 @@ onRender((ctx) => {
// gallery
ctx.font = "7px NDS7";
ctx.fillStyle = "#2c2c2c";
ctx.fillStyle = pressed.value === "gallery" ? "#2c2c2c" : "#282828";
fillTextCentered(
ctx,
$t("home.photoGallery"),
@@ -140,45 +142,40 @@ onRender((ctx) => {
:x="33"
:y="25 + getButtonOffset('projects')"
:opacity="getOpacity('projects')"
:image="assets.images.home.bottomScreen.buttons.game"
:image="getButtonImage('projects')"
/>
<Button
:x="32"
:y="72 + getButtonOffset('contact')"
:opacity="getOpacity('contact')"
:image="assets.images.home.bottomScreen.buttons.contact"
:image="getButtonImage('contact')"
/>
<Button
:x="128"
:y="72 + getButtonOffset('gallery')"
:opacity="getOpacity('gallery')"
:image="assets.images.home.bottomScreen.buttons.gallery"
:image="getButtonImage('gallery')"
/>
<Button
:x="33"
:y="121"
:opacity="getOpacity()"
:image="assets.images.home.bottomScreen.buttons.gamePak"
/>
<Button :x="33" :y="121" :opacity="getOpacity()" :image="assets.gamePak" />
<Button
:x="10"
:y="175 + getButtonOffset('theme')"
:opacity="getOpacity('theme')"
:image="assets.images.home.bottomScreen.buttons.theme"
:image="getButtonImage('theme')"
/>
<Button
:x="117"
:y="170 + getButtonOffset('settings')"
:opacity="getOpacity('settings')"
:image="assets.images.home.bottomScreen.buttons.settings"
:image="getButtonImage('settings')"
/>
<Button
:x="235"
:y="175 + getButtonOffset('achievements')"
:opacity="getOpacity('achievements')"
:image="assets.images.home.bottomScreen.buttons.achievements"
:image="getButtonImage('achievements')"
/>
<Selector :rect="selectorPosition" :opacity="getOpacity()" />

View File

@@ -4,6 +4,9 @@ const props = withDefaults(
x: number;
y: number;
opacity?: number;
pressed: string | null;
isAnyOtherMenuOpen: boolean;
submenuExtraOffsetY: number;
}>(),
{
opacity: 1,
@@ -12,46 +15,29 @@ const props = withDefaults(
const { onRender } = useScreen();
const settingsStore = useSettingsStore();
const menusContext = inject<{
isSubmenuSelected: ComputedRef<boolean>;
selectedSubmenuParent: ComputedRef<string | null>;
selectedSubmenuExtraOffsetY: ComputedRef<number>;
}>("menusContext")!;
const { assets } = useAssets();
const store = useSettingsStore();
const { assets } = useAssets((a) => a.images.settings.topScreen.clock);
const isOpen = computed(
() => settingsStore.currentMenu === "clock" && settingsStore.menuExpanded,
() => store.currentMenu === "clock" && store.menuExpanded,
);
const isAnyOtherMenuOpen = computed(() => {
if (settingsStore.currentSubMenu) {
return !settingsStore.currentSubMenu.startsWith("clock");
}
if (menusContext.isSubmenuSelected.value) {
return menusContext.selectedSubmenuParent.value !== "clock";
}
return false;
});
const animation = useMenuAnimation("clock", isOpen);
const submenuExtraOffset = (submenu: string) =>
settingsStore.selectedButton === submenu
? menusContext.selectedSubmenuExtraOffsetY.value
: 0;
store.selectedButton === submenu ? props.submenuExtraOffsetY : 0;
onRender((ctx) => {
ctx.globalAlpha = props.opacity;
ctx.translate(props.x, props.y);
if (isOpen.value || animation.playing) {
assets.images.settings.topScreen.clock.time.draw(
(props.pressed === "clockTime" ? assets.timePressed : assets.time).draw(
ctx,
48 - animation.stage2Offset,
-48 + animation.stage1Offset + submenuExtraOffset("clockTime"),
);
assets.images.settings.topScreen.clock.date.draw(
(props.pressed === "clockDate" ? assets.datePressed : assets.date).draw(
ctx,
0,
-96 +
@@ -59,17 +45,26 @@ onRender((ctx) => {
animation.stage1Offset +
submenuExtraOffset("clockDate"),
);
assets.images.settings.topScreen.clock.achievements.draw(
(props.pressed === "clockAchievements"
? assets.achievementsPressed
: assets.achievements
).draw(
ctx,
0,
-48 + animation.stage1Offset + submenuExtraOffset("clockAchievements"),
);
assets.images.settings.topScreen.clock.clockActive.draw(ctx, 0, 0);
} else if (isAnyOtherMenuOpen.value) {
assets.images.settings.topScreen.clock.clockDisabled.draw(ctx, 0, 0);
(props.pressed === "clock" ? assets.clockPressed : assets.clockActive).draw(
ctx,
0,
0,
);
} else if (props.pressed === "clock") {
assets.clockPressed.draw(ctx, 0, 0);
} else if (props.isAnyOtherMenuOpen) {
assets.clockDisabled.draw(ctx, 0, 0);
} else {
assets.images.settings.topScreen.clock.clock.draw(ctx, 0, 0);
assets.clock.draw(ctx, 0, 0);
}
});

View File

@@ -19,20 +19,20 @@ import TouchScreenTapTap from "./TouchScreen/TapTap.vue";
import Selector from "~/components/Common/ButtonSelector.vue";
const app = useAppStore();
const settingsStore = useSettingsStore();
const store = useSettingsStore();
const { assets } = useAssets();
const { onRender } = useScreen();
onRender((ctx) => {
if (settingsStore.submenuBackground.opacity > 0) {
ctx.globalAlpha = settingsStore.submenuBackground.opacity;
ctx.translate(0, settingsStore.submenuBackground.offsetY);
if (store.submenuBackground.opacity > 0) {
ctx.globalAlpha = store.submenuBackground.opacity;
ctx.translate(0, store.submenuBackground.offsetY);
assets.images.home.topScreen.background.draw(ctx, 0, 0);
}
});
if (app.previousScreen === "home") {
settingsStore.selectedButton = "options";
store.selectedButton = "options";
}
const isMainMenu = (button: string): button is SettingsMenu =>
@@ -47,7 +47,7 @@ const getParentMenu = (submenu: string): SettingsMenu => {
return match[1] as SettingsMenu;
};
const { select, selected, selectorPosition } = useButtonNavigation({
const { select, selected, pressed, selectorPosition } = useButtonNavigation({
buttons: {
options: [31, 119, 49, 49],
optionsLanguage: [31, 71, 49, 49],
@@ -67,16 +67,15 @@ const { select, selected, selectorPosition } = useButtonNavigation({
touchScreen: [175, 119, 49, 49],
},
initialButton: settingsStore.selectedButton,
initialButton: store.selectedButton,
onActivate: (buttonName) => {
if (isSubMenu(buttonName)) {
settingsStore.openSubMenu(buttonName);
store.openSubMenu(buttonName);
} else {
if (buttonName === "options") select("optionsLanguage");
if (buttonName === "clock") select("clockAchievements");
if (buttonName === "user") select("userUserName");
if (buttonName === "touchScreen")
settingsStore.openSubMenu("touchScreenTapTap");
if (buttonName === "touchScreen") store.openSubMenu("touchScreenTapTap");
}
},
navigation: {
@@ -153,17 +152,17 @@ const { select, selected, selectorPosition } = useButtonNavigation({
canClickButton: (buttonName) => {
if (isSubMenu(buttonName)) {
const parent = getParentMenu(buttonName);
return settingsStore.currentMenu === parent && settingsStore.menuExpanded;
return store.currentMenu === parent && store.menuExpanded;
}
return true;
},
disabled: computed(
() =>
settingsStore.currentSubMenu !== null ||
settingsStore.submenuTransition.opacity < 1 ||
settingsStore.isIntro ||
settingsStore.isOutro ||
settingsStore.animatingNotification,
store.currentSubMenu !== null ||
store.submenuTransition.opacity < 1 ||
store.isIntro ||
store.isOutro ||
store.animatingNotification,
),
selectorAnimation: {
duration: 0.11,
@@ -172,34 +171,34 @@ const { select, selected, selectorPosition } = useButtonNavigation({
});
const isSubmenuSelected = computed(() => isSubMenu(selected.value));
const selectedSubmenuParent = computed(() =>
isSubmenuSelected.value ? getParentMenu(selected.value) : null,
);
const selectedSubmenuExtraOffsetY = computed(
() =>
settingsStore.submenuTransition.selectorOffsetY -
settingsStore.submenuTransition.offsetY,
store.submenuTransition.selectorOffsetY - store.submenuTransition.offsetY,
);
provide("menusContext", {
isSubmenuSelected,
selectedSubmenuParent,
selectedSubmenuExtraOffsetY,
});
const isAnyOtherMenuOpen = (menu: SettingsMenu) => {
if (store.currentSubMenu) {
return !store.currentSubMenu.startsWith(menu);
}
if (isSubmenuSelected.value) {
return getParentMenu(selected.value) !== menu;
}
return false;
};
watch(
selected,
(newSelected) => {
settingsStore.selectedButton = newSelected;
store.selectedButton = newSelected;
if (settingsStore.currentSubMenu === null) {
if (store.currentSubMenu === null) {
if (isMainMenu(newSelected)) {
settingsStore.openMenu(newSelected, false);
store.openMenu(newSelected, false);
} else if (isSubMenu(newSelected)) {
const parentMenu = getParentMenu(newSelected);
if (parentMenu) {
settingsStore.openMenu(parentMenu, true);
store.openMenu(parentMenu, true);
}
}
}
@@ -231,18 +230,13 @@ const selectorXOffset = computed(() => {
switch (menu) {
case "clock":
return Math.min(0, settingsStore.menuOffsets[1]);
return Math.min(0, store.menuOffsets[1]);
case "user":
return Math.min(
0,
settingsStore.menuOffsets[1] + settingsStore.menuOffsets[2],
);
return Math.min(0, store.menuOffsets[1] + store.menuOffsets[2]);
case "touchScreen":
return Math.min(
0,
settingsStore.menuOffsets[1] +
settingsStore.menuOffsets[2] +
settingsStore.menuOffsets[3],
store.menuOffsets[1] + store.menuOffsets[2] + store.menuOffsets[3],
);
default:
return 0;
@@ -251,91 +245,81 @@ const selectorXOffset = computed(() => {
const selectorTransitionOffsetY = computed(() => {
if (isSubmenuSelected.value || selected.value === "touchScreen") {
return settingsStore.submenuTransition.selectorOffsetY;
return store.submenuTransition.selectorOffsetY;
}
return settingsStore.submenuTransition.offsetY;
return store.submenuTransition.offsetY;
});
const handleActivateB = () => {
if (
settingsStore.isIntro ||
settingsStore.isOutro ||
settingsStore.submenuTransition.opacity < 1
)
if (store.isIntro || store.isOutro || store.submenuTransition.opacity < 1)
return;
if (isSubmenuSelected.value) {
select(getParentMenu(selected.value));
} else {
settingsStore.animateOutro();
store.animateOutro();
}
};
</script>
<template>
<template v-if="!settingsStore.currentSubMenu">
<template v-if="!store.currentSubMenu">
<TouchScreenMenu
:x="
177 +
settingsStore.menuOffsets[1] +
settingsStore.menuOffsets[2] +
settingsStore.menuOffsets[3]
177 + store.menuOffsets[1] + store.menuOffsets[2] + store.menuOffsets[3]
"
:y="
121 +
settingsStore.menuYOffset +
store.menuYOffset +
(selected === 'touchScreen'
? settingsStore.submenuTransition.selectorOffsetY
: settingsStore.submenuTransition.offsetY)
? store.submenuTransition.selectorOffsetY
: store.submenuTransition.offsetY)
"
:opacity="settingsStore.submenuTransition.opacity"
:opacity="store.submenuTransition.opacity"
:pressed="pressed"
:is-any-other-menu-open="isAnyOtherMenuOpen('touchScreen')"
/>
<UserMenu
:x="129 + settingsStore.menuOffsets[1] + settingsStore.menuOffsets[2]"
:y="
121 +
settingsStore.menuYOffset +
settingsStore.submenuTransition.offsetY
"
:opacity="settingsStore.submenuTransition.opacity"
:x="129 + store.menuOffsets[1] + store.menuOffsets[2]"
:y="121 + store.menuYOffset + store.submenuTransition.offsetY"
:opacity="store.submenuTransition.opacity"
:pressed="pressed"
:is-any-other-menu-open="isAnyOtherMenuOpen('user')"
:submenu-extra-offset-y="selectedSubmenuExtraOffsetY"
/>
<ClockMenu
:x="81 + settingsStore.menuOffsets[1]"
:y="
121 +
settingsStore.menuYOffset +
settingsStore.submenuTransition.offsetY
"
:opacity="settingsStore.submenuTransition.opacity"
:x="81 + store.menuOffsets[1]"
:y="121 + store.menuYOffset + store.submenuTransition.offsetY"
:opacity="store.submenuTransition.opacity"
:pressed="pressed"
:is-any-other-menu-open="isAnyOtherMenuOpen('clock')"
:submenu-extra-offset-y="selectedSubmenuExtraOffsetY"
/>
<OptionsMenu
:x="33"
:y="
121 +
settingsStore.menuYOffset +
settingsStore.submenuTransition.offsetY
"
:opacity="settingsStore.submenuTransition.opacity"
:y="121 + store.menuYOffset + store.submenuTransition.offsetY"
:opacity="store.submenuTransition.opacity"
:pressed="pressed"
:is-any-other-menu-open="isAnyOtherMenuOpen('options')"
:submenu-extra-offset-y="selectedSubmenuExtraOffsetY"
/>
<Selector
:rect="[
selectorPosition[0] + selectorXOffset,
selectorPosition[1] +
settingsStore.menuYOffset +
selectorTransitionOffsetY,
selectorPosition[1] + store.menuYOffset + selectorTransitionOffsetY,
selectorPosition[2],
selectorPosition[3],
]"
:opacity="settingsStore.submenuTransition.opacity"
:opacity="store.submenuTransition.opacity"
/>
<CommonButtons
:y-offset="settingsStore.barOffsetY + settingsStore.submenuButtonsOffsetY"
:y-offset="store.barOffsetY + store.submenuButtonsOffsetY"
:b-label="isSubmenuSelected ? $t('common.goBack') : $t('common.quit')"
:a-label="$t('common.select')"
@activate-b="handleActivateB()"
/>
</template>
<component :is="viewComponents[settingsStore.currentSubMenu]" v-else />
<component :is="viewComponents[store.currentSubMenu]" v-else />
</template>

View File

@@ -246,13 +246,21 @@ onRender((ctx) => {
onRender((ctx) => {
ctx.translate(0, intro.scoreOffsetY);
drawButton(ctx, `${$t("settings.options.2048.score")}: ${score}`, 10, 2, 118);
drawButton(
ctx,
`${$t("settings.options.2048.score")}: ${score}`,
10,
2,
118,
false,
);
drawButton(
ctx,
`${$t("settings.options.2048.highScore")}: ${savedState.value.highScore}`,
138,
2,
108,
false,
);
}, 110);

View File

@@ -4,6 +4,9 @@ const props = withDefaults(
x: number;
y: number;
opacity?: number;
pressed: string | null;
isAnyOtherMenuOpen: boolean;
submenuExtraOffsetY: number;
}>(),
{
opacity: 1,
@@ -12,46 +15,32 @@ const props = withDefaults(
const { onRender } = useScreen();
const settingsStore = useSettingsStore();
const menusContext = inject<{
isSubmenuSelected: ComputedRef<boolean>;
selectedSubmenuParent: ComputedRef<string | null>;
selectedSubmenuExtraOffsetY: ComputedRef<number>;
}>("menusContext")!;
const { assets } = useAssets();
const store = useSettingsStore();
const { assets } = useAssets((a) => a.images.settings.topScreen.options);
const isOpen = computed(
() => settingsStore.currentMenu === "options" && settingsStore.menuExpanded,
() => store.currentMenu === "options" && store.menuExpanded,
);
const isAnyOtherMenuOpen = computed(() => {
if (settingsStore.currentSubMenu) {
return !settingsStore.currentSubMenu.startsWith("options");
}
if (menusContext.isSubmenuSelected.value) {
return menusContext.selectedSubmenuParent.value !== "options";
}
return false;
});
const animation = useMenuAnimation("options", isOpen);
const submenuExtraOffset = (submenu: string) =>
settingsStore.selectedButton === submenu
? menusContext.selectedSubmenuExtraOffsetY.value
: 0;
store.selectedButton === submenu ? props.submenuExtraOffsetY : 0;
onRender((ctx) => {
ctx.globalAlpha = props.opacity;
ctx.translate(props.x, props.y);
if (isOpen.value || animation.playing) {
assets.images.settings.topScreen.options._2048.draw(
(props.pressed === "options2048" ? assets._2048Pressed : assets._2048).draw(
ctx,
48 - animation.stage2Offset,
-48 + animation.stage1Offset + submenuExtraOffset("options2048"),
);
assets.images.settings.topScreen.options.renderingMode.draw(
(props.pressed === "optionsRenderingMode"
? assets.renderingModePressed
: assets.renderingMode
).draw(
ctx,
0,
-96 +
@@ -59,17 +48,25 @@ onRender((ctx) => {
animation.stage1Offset +
submenuExtraOffset("optionsRenderingMode"),
);
assets.images.settings.topScreen.options.language.draw(
(props.pressed === "optionsLanguage"
? assets.languagePressed
: assets.language
).draw(
ctx,
0,
-48 + animation.stage1Offset + submenuExtraOffset("optionsLanguage"),
);
assets.images.settings.topScreen.options.optionsActive.draw(ctx, 0, 0);
} else if (isAnyOtherMenuOpen.value) {
assets.images.settings.topScreen.options.optionsDisabled.draw(ctx, 0, 0);
(props.pressed === "options"
? assets.optionsPressed
: assets.optionsActive
).draw(ctx, 0, 0);
} else if (props.pressed === "options") {
assets.optionsPressed.draw(ctx, 0, 0);
} else if (props.isAnyOtherMenuOpen) {
assets.optionsDisabled.draw(ctx, 0, 0);
} else {
assets.images.settings.topScreen.options.options.draw(ctx, 0, 0);
assets.options.draw(ctx, 0, 0);
}
});

View File

@@ -4,6 +4,8 @@ const props = withDefaults(
x: number;
y: number;
opacity?: number;
pressed: string | null;
isAnyOtherMenuOpen: boolean;
}>(),
{
opacity: 1,
@@ -12,39 +14,16 @@ const props = withDefaults(
const { onRender } = useScreen();
const settingsStore = useSettingsStore();
const menusContext = inject<{
isSubmenuSelected: ComputedRef<boolean>;
selectedSubmenuParent: ComputedRef<string | null>;
}>("menusContext")!;
const { assets } = useAssets();
// TODO: i don't like this
const isAnyOtherMenuOpen = computed(() => {
if (settingsStore.currentSubMenu) {
return !settingsStore.currentSubMenu.startsWith("touchScreen");
}
if (menusContext.isSubmenuSelected.value) {
return menusContext.selectedSubmenuParent.value !== "touchScreen";
}
return false;
});
const { assets } = useAssets((a) => a.images.settings.topScreen.touchScreen);
onRender((ctx) => {
ctx.globalAlpha = props.opacity;
if (isAnyOtherMenuOpen.value) {
assets.images.settings.topScreen.touchScreen.touchScreenDisabled.draw(
ctx,
props.x,
props.y,
);
if (props.pressed === "touchScreen") {
assets.touchScreenPressed.draw(ctx, props.x, props.y);
} else if (props.isAnyOtherMenuOpen) {
assets.touchScreenDisabled.draw(ctx, props.x, props.y);
} else {
assets.images.settings.topScreen.touchScreen.touchScreen.draw(
ctx,
props.x,
props.y,
);
assets.touchScreen.draw(ctx, props.x, props.y);
}
});

View File

@@ -393,6 +393,7 @@ onRender((ctx) => {
10,
2,
88,
false,
);
drawButton(
ctx,
@@ -400,6 +401,7 @@ onRender((ctx) => {
108,
2,
88,
false,
);
drawButton(
ctx,
@@ -407,6 +409,7 @@ onRender((ctx) => {
206,
2,
40,
false,
);
}, 110);

View File

@@ -4,6 +4,9 @@ const props = withDefaults(
x: number;
y: number;
opacity?: number;
pressed: string | null;
isAnyOtherMenuOpen: boolean;
submenuExtraOffsetY: number;
}>(),
{
opacity: 1,
@@ -12,51 +15,37 @@ const props = withDefaults(
const { onRender } = useScreen();
const settingsStore = useSettingsStore();
const menusContext = inject<{
isSubmenuSelected: ComputedRef<boolean>;
selectedSubmenuParent: ComputedRef<string | null>;
selectedSubmenuExtraOffsetY: ComputedRef<number>;
}>("menusContext")!;
const { assets } = useAssets();
const store = useSettingsStore();
const { assets } = useAssets((a) => a.images.settings.topScreen.user);
const isOpen = computed(
() => settingsStore.currentMenu === "user" && settingsStore.menuExpanded,
() => store.currentMenu === "user" && store.menuExpanded,
);
const isAnyOtherMenuOpen = computed(() => {
if (settingsStore.currentSubMenu) {
return !settingsStore.currentSubMenu.startsWith("user");
}
if (menusContext.isSubmenuSelected.value) {
return menusContext.selectedSubmenuParent.value !== "user";
}
return false;
});
const animation = useMenuAnimation("user", isOpen);
const submenuExtraOffset = (submenu: string) =>
settingsStore.selectedButton === submenu
? menusContext.selectedSubmenuExtraOffsetY.value
: 0;
store.selectedButton === submenu ? props.submenuExtraOffsetY : 0;
onRender((ctx) => {
ctx.globalAlpha = props.opacity;
ctx.translate(props.x, props.y);
if (isOpen.value || animation.playing) {
assets.images.settings.topScreen.user.birthday.draw(
(props.pressed === "userBirthday"
? assets.birthdayPressed
: assets.birthday
).draw(
ctx,
-48 + animation.stage2Offset,
-48 + animation.stage1Offset + submenuExtraOffset("userBirthday"),
);
assets.images.settings.topScreen.user.snake.draw(
(props.pressed === "userSnake" ? assets.snakePressed : assets.snake).draw(
ctx,
48 - animation.stage2Offset,
-48 + animation.stage1Offset + submenuExtraOffset("userSnake"),
);
assets.images.settings.topScreen.user.color.draw(
(props.pressed === "userColor" ? assets.colorPressed : assets.color).draw(
ctx,
0,
-96 +
@@ -64,17 +53,26 @@ onRender((ctx) => {
animation.stage1Offset +
submenuExtraOffset("userColor"),
);
assets.images.settings.topScreen.user.userName.draw(
(props.pressed === "userUserName"
? assets.userNamePressed
: assets.userName
).draw(
ctx,
0,
-48 + animation.stage1Offset + submenuExtraOffset("userUserName"),
);
assets.images.settings.topScreen.user.userActive.draw(ctx, 0, 0);
} else if (isAnyOtherMenuOpen.value) {
assets.images.settings.topScreen.user.userDisabled.draw(ctx, 0, 0);
(props.pressed === "user" ? assets.userPressed : assets.userActive).draw(
ctx,
0,
0,
);
} else if (props.pressed === "user") {
assets.userPressed.draw(ctx, 0, 0);
} else if (props.isAnyOtherMenuOpen) {
assets.userDisabled.draw(ctx, 0, 0);
} else {
assets.images.settings.topScreen.user.user.draw(ctx, 0, 0);
assets.user.draw(ctx, 0, 0);
}
});

View File

@@ -292,13 +292,14 @@ onRender((ctx) => {
onRender((ctx) => {
ctx.translate(0, intro.scoreOffsetY);
drawButton(ctx, $t("settings.user.snake.score", { score }), 10, 2, 118);
drawButton(ctx, $t("settings.user.snake.score", { score }), 10, 2, 118, true);
drawButton(
ctx,
$t("settings.user.snake.best", { best: highScore.value }),
138,
2,
108,
true,
);
}, 110);

View File

@@ -210,16 +210,8 @@ export const useButtonNavigation = <T extends Record<string, Rect>>({
}
});
const handleMouseUp = () => {
useMouseUp(() => {
pressedButton.value = null;
};
onMounted(() => {
document.addEventListener("mouseup", handleMouseUp);
});
onUnmounted(() => {
document.removeEventListener("mouseup", handleMouseUp);
});
onClick((x: number, y: number) => {

View File

@@ -162,6 +162,7 @@ export const drawButton = (
x: number,
y: number,
width: number,
pressed: boolean,
) => {
const { assets } = useAssets((a) => a.images.common);
@@ -175,13 +176,21 @@ export const drawButton = (
ctx.save();
ctx.translate(x, y);
assets.buttonLeft.draw(ctx, 0, 0);
(pressed ? assets.buttonLeftPressed : assets.buttonLeft).draw(ctx, 0, 0);
for (let i = 0; i < bodyCount; i++) {
assets.buttonBody.draw(ctx, LEFT_WIDTH + i * BODY_WIDTH, 0);
(pressed ? assets.buttonBodyPressed : assets.buttonBody).draw(
ctx,
LEFT_WIDTH + i * BODY_WIDTH,
0,
);
}
assets.buttonRight.draw(ctx, width - RIGHT_WIDTH, 0);
(pressed ? assets.buttonRightPressed : assets.buttonRight).draw(
ctx,
width - RIGHT_WIDTH,
0,
);
ctx.textBaseline = "top";
ctx.font = "10px NDS10";

View File

@@ -15,3 +15,13 @@ export const mapKeyToNDS = (key: string): string | null => {
key = key.length === 1 ? key.toUpperCase() : key;
return KEY_TO_NDS_BUTTON[key] ?? null;
};
export const useMouseUp = (callback: () => void) => {
onMounted(() => {
document.addEventListener("mouseup", callback);
});
onUnmounted(() => {
document.removeEventListener("mouseup", callback);
});
};

View File

@@ -161,9 +161,9 @@
"actions": {
"open": "Open",
"copy": "Copy",
"githubProfile": "Github profile",
"git": "Github profile",
"email": "Email",
"websiteLink": "Website link",
"linkedin": "LinkedIn link",
"cv": "CV"
},
"copiedToClipboard": "{item} copied to clipboard",

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 B

After

Width:  |  Height:  |  Size: 88 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 B

After

Width:  |  Height:  |  Size: 90 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 B

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

View File

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B