feat(settings/options/rendering-mode): implement
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { onRender } = useScreen();
|
const { onRender } = useScreen();
|
||||||
|
|
||||||
|
const app = useAppStore();
|
||||||
const store = useHomeStore();
|
const store = useHomeStore();
|
||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
@@ -44,7 +45,11 @@ onRender((ctx) => {
|
|||||||
|
|
||||||
// icons
|
// icons
|
||||||
assets.images.home.topScreen.statusBar.gbaDisplay.draw(ctx, 210, 2);
|
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);
|
assets.images.home.topScreen.statusBar.battery.draw(ctx, 242, 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -394,6 +394,8 @@ const handleMouseUp = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
app.ready = true;
|
||||||
|
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
renderer.instance.domElement.addEventListener("mousedown", handleClick);
|
renderer.instance.domElement.addEventListener("mousedown", handleClick);
|
||||||
renderer.instance.domElement.addEventListener("mouseup", handleMouseUp);
|
renderer.instance.domElement.addEventListener("mouseup", handleMouseUp);
|
||||||
|
|||||||
@@ -1,13 +1,110 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { until } from "@vueuse/core";
|
||||||
|
|
||||||
|
const app = useAppStore();
|
||||||
|
const store = useSettingsStore();
|
||||||
|
const confirmationModal = useConfirmationModal();
|
||||||
const { onRender } = useScreen();
|
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) => {
|
onRender((ctx) => {
|
||||||
ctx.font = "10px NDS10";
|
assets.images.home.topScreen.background.draw(ctx, 0, 0);
|
||||||
ctx.fillStyle = "#000000";
|
|
||||||
ctx.fillText("Startup", 10, 20);
|
|
||||||
});
|
|
||||||
|
|
||||||
defineOptions({
|
ctx.font = "10px NDS10";
|
||||||
render: () => null,
|
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>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CommonButtons
|
||||||
|
:y-offset="confirmationModal.buttonsYOffset"
|
||||||
|
b-label="Cancel"
|
||||||
|
a-label="Confirm"
|
||||||
|
@activate-b="handleCancel"
|
||||||
|
@activate-a="handleConfirm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CommonButtonSelector :rect="selectorPosition" />
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { onRender } = useScreen();
|
const { onRender } = useScreen();
|
||||||
|
|
||||||
|
const app = useAppStore();
|
||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
@@ -39,7 +40,11 @@ onRender((ctx) => {
|
|||||||
|
|
||||||
// icons
|
// icons
|
||||||
assets.images.home.topScreen.statusBar.gbaDisplay.draw(ctx, 210, 2);
|
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);
|
assets.images.home.topScreen.statusBar.battery.draw(ctx, 242, 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Screen as NDSScreen } from "#components";
|
import type { Screen as NDSScreen } from "#components";
|
||||||
|
|
||||||
const ENABLE_3D = true;
|
|
||||||
|
|
||||||
type ScreenInstance = InstanceType<typeof NDSScreen>;
|
type ScreenInstance = InstanceType<typeof NDSScreen>;
|
||||||
|
|
||||||
const { isReady } = useAssets();
|
const { isReady } = useAssets();
|
||||||
@@ -15,6 +13,8 @@ const bottomScreen = useTemplateRef<ScreenInstance>("bottomScreen");
|
|||||||
const topScreenCanvas = computed(() => topScreen.value?.canvas ?? null);
|
const topScreenCanvas = computed(() => topScreen.value?.canvas ?? null);
|
||||||
const bottomScreenCanvas = computed(() => bottomScreen.value?.canvas ?? null);
|
const bottomScreenCanvas = computed(() => bottomScreen.value?.canvas ?? null);
|
||||||
|
|
||||||
|
const a = useAchievementsStore();
|
||||||
|
|
||||||
const keyToButton: Record<string, string> = {
|
const keyToButton: Record<string, string> = {
|
||||||
ArrowUp: "UP",
|
ArrowUp: "UP",
|
||||||
ArrowDown: "DOWN",
|
ArrowDown: "DOWN",
|
||||||
@@ -22,8 +22,8 @@ const keyToButton: Record<string, string> = {
|
|||||||
ArrowRight: "RIGHT",
|
ArrowRight: "RIGHT",
|
||||||
d: "A",
|
d: "A",
|
||||||
s: "B",
|
s: "B",
|
||||||
w: "X",
|
z: "X",
|
||||||
a: "Y",
|
q: "Y",
|
||||||
" ": "SELECT",
|
" ": "SELECT",
|
||||||
Enter: "START",
|
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
|
// 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
|
// like who choose Nuxt to build such an app
|
||||||
useKeyDown((key) => {
|
useKeyDown((key) => {
|
||||||
if (ENABLE_3D) return;
|
if (app.settings.renderingMode === "3d") return;
|
||||||
const button = keyToButton[key];
|
const button = keyToButton[key];
|
||||||
if (button) {
|
if (button) {
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new KeyboardEvent("keydown", { key: `NDS_${button}` }),
|
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) => {
|
useKeyUp((key) => {
|
||||||
if (ENABLE_3D) return;
|
if (app.settings.renderingMode === "3d") return;
|
||||||
const button = keyToButton[key];
|
const button = keyToButton[key];
|
||||||
if (button) {
|
if (button) {
|
||||||
window.dispatchEvent(new KeyboardEvent("keyup", { key: `NDS_${button}` }));
|
window.dispatchEvent(new KeyboardEvent("keyup", { key: `NDS_${button}` }));
|
||||||
@@ -54,7 +70,11 @@ useKeyUp((key) => {
|
|||||||
<template>
|
<template>
|
||||||
<LoadingScreen v-if="!isReady" />
|
<LoadingScreen v-if="!isReady" />
|
||||||
<div v-else>
|
<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]" />
|
<TresPerspectiveCamera :args="[45, 1, 0.001, 1000]" />
|
||||||
|
|
||||||
<TresAmbientLight />
|
<TresAmbientLight />
|
||||||
@@ -67,7 +87,11 @@ useKeyUp((key) => {
|
|||||||
/>
|
/>
|
||||||
</TresCanvas>
|
</TresCanvas>
|
||||||
|
|
||||||
<div :style="{ visibility: ENABLE_3D ? 'hidden' : 'visible' }">
|
<div
|
||||||
|
:style="{
|
||||||
|
visibility: app.settings.renderingMode === '3d' ? 'hidden' : 'visible',
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Screen ref="topScreen">
|
<Screen ref="topScreen">
|
||||||
<IntroTopScreen v-if="!app.booted" />
|
<IntroTopScreen v-if="!app.booted" />
|
||||||
@@ -89,6 +113,7 @@ useKeyUp((key) => {
|
|||||||
<ProjectsBottomScreen v-else-if="app.screen === 'projects'" />
|
<ProjectsBottomScreen v-else-if="app.screen === 'projects'" />
|
||||||
<SettingsBottomScreen v-else-if="app.screen === 'settings'" />
|
<SettingsBottomScreen v-else-if="app.screen === 'settings'" />
|
||||||
<GalleryBottomScreen v-else-if="app.screen === 'gallery'" />
|
<GalleryBottomScreen v-else-if="app.screen === 'gallery'" />
|
||||||
|
<AchievementsBottomScreen v-else-if="app.screen === 'achievements'" />
|
||||||
</Screen>
|
</Screen>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ const settingsSchema = z.object({
|
|||||||
col: z.number(),
|
col: z.number(),
|
||||||
row: z.number(),
|
row: z.number(),
|
||||||
}),
|
}),
|
||||||
|
renderingMode: z.enum(["3d", "2d"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Settings = z.infer<typeof settingsSchema>;
|
type Settings = z.infer<typeof settingsSchema>;
|
||||||
|
|
||||||
const defaultSettings = (): Settings => ({
|
const defaultSettings = (): Settings => ({
|
||||||
color: { col: 0, row: 0 },
|
color: { col: 0, row: 0 },
|
||||||
|
renderingMode: "3d",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useAppStore = defineStore("app", {
|
export const useAppStore = defineStore("app", {
|
||||||
@@ -27,7 +29,8 @@ export const useAppStore = defineStore("app", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
booted: false,
|
ready: false,
|
||||||
|
booted: true,
|
||||||
settings,
|
settings,
|
||||||
previousScreen: "home" as AppScreen,
|
previousScreen: "home" as AppScreen,
|
||||||
screen: "home" as AppScreen,
|
screen: "home" as AppScreen,
|
||||||
@@ -40,6 +43,12 @@ export const useAppStore = defineStore("app", {
|
|||||||
this.settings.color = { col, row };
|
this.settings.color = { col, row };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setRenderingMode(mode: Settings["renderingMode"]) {
|
||||||
|
this.ready = mode === "2d";
|
||||||
|
this.settings.renderingMode = mode;
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
navigateTo(screen: AppScreen) {
|
navigateTo(screen: AppScreen) {
|
||||||
this.previousScreen = this.screen;
|
this.previousScreen = this.screen;
|
||||||
this.screen = screen;
|
this.screen = screen;
|
||||||
|
|||||||
@@ -13,6 +13,23 @@ export const fillTextCentered = (
|
|||||||
return measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent + 1;
|
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 = (
|
export const fillTextHCentered = (
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
text: string,
|
text: string,
|
||||||
@@ -25,6 +42,20 @@ export const fillTextHCentered = (
|
|||||||
ctx.fillText(text, textX, y);
|
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 = (
|
export const fillTextWordWrapped = (
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
text: string,
|
text: string,
|
||||||
|
|||||||
BIN
public/nds/images/home/top-screen/status-bar/2d-mode.webp
Normal file
|
After Width: | Height: | Size: 82 B |
BIN
public/nds/images/home/top-screen/status-bar/3d-mode.webp
Normal file
|
After Width: | Height: | Size: 80 B |
|
After Width: | Height: | Size: 48 B |
|
After Width: | Height: | Size: 50 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 168 B |
|
After Width: | Height: | Size: 88 B |