feat(settings/options/rendering-mode): implement

This commit is contained in:
2026-01-29 17:52:26 +01:00
parent 8305f613ef
commit 5e37d47eb1
14 changed files with 191 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
const { onRender } = useScreen();
const app = useAppStore();
const store = useHomeStore();
const { assets } = useAssets();
@@ -44,7 +45,11 @@ onRender((ctx) => {
// icons
assets.images.home.topScreen.statusBar.gbaDisplay.draw(ctx, 210, 2);
assets.images.home.topScreen.statusBar.startupMode.draw(ctx, 226, 2);
if (app.settings.renderingMode === "3d") {
assets.images.home.topScreen.statusBar._3dMode.draw(ctx, 226, 2);
} else {
assets.images.home.topScreen.statusBar._2dMode.draw(ctx, 226, 2);
}
assets.images.home.topScreen.statusBar.battery.draw(ctx, 242, 4);
});

View File

@@ -394,6 +394,8 @@ const handleMouseUp = () => {
};
onMounted(() => {
app.ready = true;
if (renderer) {
renderer.instance.domElement.addEventListener("mousedown", handleClick);
renderer.instance.domElement.addEventListener("mouseup", handleMouseUp);

View File

@@ -1,13 +1,110 @@
<script setup lang="ts">
import { until } from "@vueuse/core";
const app = useAppStore();
const store = useSettingsStore();
const confirmationModal = useConfirmationModal();
const { onRender } = useScreen();
const { assets } = useAssets();
const renderingModeAssets =
assets.images.settings.bottomScreen.options.renderingMode;
const { selected, selectorPosition } = useButtonNavigation({
buttons: {
_3dMode: [11, 27, 233, 74],
_2dMode: [11, 91, 233, 74],
},
initialButton: app.settings.renderingMode === "3d" ? "_3dMode" : "_2dMode",
navigation: {
_3dMode: { down: "_2dMode" },
_2dMode: { up: "_3dMode" },
},
selectorAnimation: {
ease: "none",
duration: 0.065,
},
});
const handleCancel = () => {
store.closeSubMenu();
};
const handleConfirm = () => {
const mode = selected.value === "_3dMode" ? "3d" : "2d";
app.setRenderingMode(mode);
const showConfirmation = () => {
confirmationModal.open({
text: `Rendering mode set to ${mode === "3d" ? "3D" : "2D"}`,
onClosed: () => {
store.closeSubMenu();
},
});
setTimeout(() => confirmationModal.close(), 2000);
};
until(() => app.ready)
.toBeTruthy()
.then(showConfirmation);
};
onRender((ctx) => {
ctx.font = "10px NDS10";
ctx.fillStyle = "#000000";
ctx.fillText("Startup", 10, 20);
});
assets.images.home.topScreen.background.draw(ctx, 0, 0);
defineOptions({
render: () => null,
ctx.font = "10px NDS10";
ctx.textBaseline = "top";
const drawButton = (
title: string,
logo: AtlasImage,
y: number,
active: boolean,
) => {
ctx.save();
ctx.translate(16, y);
if (active) {
renderingModeAssets.buttonActive.draw(ctx, 0, 0, { colored: true });
} else {
renderingModeAssets.button.draw(ctx, 0, 0);
}
const buttonWidth = renderingModeAssets.button.rect.width;
ctx.fillStyle = "#000000";
fillImageTextHCentered(ctx, logo, title, 0, 4, buttonWidth, 4);
ctx.fillStyle = "#282828";
const text =
"The Main Menu will appear\nautomatically when you turn\nthe power on.";
fillTextHCenteredMultiline(ctx, text, 0, y, buttonWidth, 15);
ctx.restore();
};
drawButton(
"3D Mode",
renderingModeAssets._3dMode,
32,
selected.value === "_3dMode",
);
drawButton(
"2D Mode",
renderingModeAssets._2dMode,
96,
selected.value === "_2dMode",
);
});
</script>
<template>
<CommonButtons
:y-offset="confirmationModal.buttonsYOffset"
b-label="Cancel"
a-label="Confirm"
@activate-b="handleCancel"
@activate-a="handleConfirm"
/>
<CommonButtonSelector :rect="selectorPosition" />
</template>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
const { onRender } = useScreen();
const app = useAppStore();
const { assets } = useAssets();
onRender((ctx) => {
@@ -39,7 +40,11 @@ onRender((ctx) => {
// icons
assets.images.home.topScreen.statusBar.gbaDisplay.draw(ctx, 210, 2);
assets.images.home.topScreen.statusBar.startupMode.draw(ctx, 226, 2);
if (app.settings.renderingMode === "3d") {
assets.images.home.topScreen.statusBar._3dMode.draw(ctx, 226, 2);
} else {
assets.images.home.topScreen.statusBar._2dMode.draw(ctx, 226, 2);
}
assets.images.home.topScreen.statusBar.battery.draw(ctx, 242, 4);
});

View File

@@ -1,8 +1,6 @@
<script setup lang="ts">
import type { Screen as NDSScreen } from "#components";
const ENABLE_3D = true;
type ScreenInstance = InstanceType<typeof NDSScreen>;
const { isReady } = useAssets();
@@ -15,6 +13,8 @@ const bottomScreen = useTemplateRef<ScreenInstance>("bottomScreen");
const topScreenCanvas = computed(() => topScreen.value?.canvas ?? null);
const bottomScreenCanvas = computed(() => bottomScreen.value?.canvas ?? null);
const a = useAchievementsStore();
const keyToButton: Record<string, string> = {
ArrowUp: "UP",
ArrowDown: "DOWN",
@@ -22,8 +22,8 @@ const keyToButton: Record<string, string> = {
ArrowRight: "RIGHT",
d: "A",
s: "B",
w: "X",
a: "Y",
z: "X",
q: "Y",
" ": "SELECT",
Enter: "START",
};
@@ -33,17 +33,33 @@ const keyToButton: Record<string, string> = {
// that's a bit dirty but who cares, there is a lot of dirty things going on here
// like who choose Nuxt to build such an app
useKeyDown((key) => {
if (ENABLE_3D) return;
if (app.settings.renderingMode === "3d") return;
const button = keyToButton[key];
if (button) {
window.dispatchEvent(
new KeyboardEvent("keydown", { key: `NDS_${button}` }),
);
}
// testing purpose only
if (key === "m") {
a.reset();
} else if (key === "o") {
for (const ach of ACHIEVEMENTS) {
a.unlock(ach);
}
} else if (key === "p") {
for (const ach of ACHIEVEMENTS) {
if (!a.isUnlocked(ach)) {
a.unlock(ach);
break;
}
}
}
});
useKeyUp((key) => {
if (ENABLE_3D) return;
if (app.settings.renderingMode === "3d") return;
const button = keyToButton[key];
if (button) {
window.dispatchEvent(new KeyboardEvent("keyup", { key: `NDS_${button}` }));
@@ -54,7 +70,11 @@ useKeyUp((key) => {
<template>
<LoadingScreen v-if="!isReady" />
<div v-else>
<TresCanvas v-if="ENABLE_3D" window-size clear-color="#181818">
<TresCanvas
v-if="app.settings.renderingMode === '3d'"
window-size
clear-color="#181818"
>
<TresPerspectiveCamera :args="[45, 1, 0.001, 1000]" />
<TresAmbientLight />
@@ -67,7 +87,11 @@ useKeyUp((key) => {
/>
</TresCanvas>
<div :style="{ visibility: ENABLE_3D ? 'hidden' : 'visible' }">
<div
:style="{
visibility: app.settings.renderingMode === '3d' ? 'hidden' : 'visible',
}"
>
<div>
<Screen ref="topScreen">
<IntroTopScreen v-if="!app.booted" />
@@ -89,6 +113,7 @@ useKeyUp((key) => {
<ProjectsBottomScreen v-else-if="app.screen === 'projects'" />
<SettingsBottomScreen v-else-if="app.screen === 'settings'" />
<GalleryBottomScreen v-else-if="app.screen === 'gallery'" />
<AchievementsBottomScreen v-else-if="app.screen === 'achievements'" />
</Screen>
</div>
</div>

View File

@@ -8,12 +8,14 @@ const settingsSchema = z.object({
col: z.number(),
row: z.number(),
}),
renderingMode: z.enum(["3d", "2d"]),
});
type Settings = z.infer<typeof settingsSchema>;
const defaultSettings = (): Settings => ({
color: { col: 0, row: 0 },
renderingMode: "3d",
});
export const useAppStore = defineStore("app", {
@@ -27,7 +29,8 @@ export const useAppStore = defineStore("app", {
}
return {
booted: false,
ready: false,
booted: true,
settings,
previousScreen: "home" as AppScreen,
screen: "home" as AppScreen,
@@ -40,6 +43,12 @@ export const useAppStore = defineStore("app", {
this.settings.color = { col, row };
},
setRenderingMode(mode: Settings["renderingMode"]) {
this.ready = mode === "2d";
this.settings.renderingMode = mode;
this.save();
},
navigateTo(screen: AppScreen) {
this.previousScreen = this.screen;
this.screen = screen;

View File

@@ -13,6 +13,23 @@ export const fillTextCentered = (
return measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent + 1;
};
export const fillImageTextHCentered = (
ctx: CanvasRenderingContext2D,
image: AtlasImage,
text: string,
x: number,
y: number,
width: number,
gap: number,
) => {
const { actualBoundingBoxRight: textWidth } = ctx.measureText(text);
const totalWidth = textWidth + gap + image.rect.width;
const groupX = Math.floor(x + width / 2 - totalWidth / 2);
image.draw(ctx, groupX, y);
ctx.fillText(text, groupX + gap + image.rect.width, y);
};
export const fillTextHCentered = (
ctx: CanvasRenderingContext2D,
text: string,
@@ -25,6 +42,20 @@ export const fillTextHCentered = (
ctx.fillText(text, textX, y);
};
export const fillTextHCenteredMultiline = (
ctx: CanvasRenderingContext2D,
text: string,
x: number,
y: number,
width: number,
lineHeight: number,
) => {
const lines = text.split("\n");
for (let i = 0, y = 20; i < lines.length; i += 1, y += lineHeight) {
fillTextHCentered(ctx, lines[i]!, 0, y, width);
}
};
export const fillTextWordWrapped = (
ctx: CanvasRenderingContext2D,
text: string,

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B