BREAKING CHANGE: vanilla ts three scene setup
34
.github/workflows/ci.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, 3d-nds]
|
||||
pull_request:
|
||||
branches: [main, 3d-nds]
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: pnpm setup
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: node setup
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 25
|
||||
cache: "pnpm"
|
||||
|
||||
- name: install
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: build
|
||||
run: pnpm build
|
||||
47
.gitignore
vendored
@@ -1,33 +1,26 @@
|
||||
# generated
|
||||
public/images/projects
|
||||
|
||||
# temporary
|
||||
__old
|
||||
|
||||
# ESlint
|
||||
.eslintcache
|
||||
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
OLD
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
OLD
|
||||
.test
|
||||
75
README.md
@@ -1,75 +0,0 @@
|
||||
# Nuxt Minimal Starter
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<NuxtPage />
|
||||
</template>
|
||||
@@ -1,32 +0,0 @@
|
||||
@font-face {
|
||||
font-family: "NDS7";
|
||||
src: url("/assets/fonts/nds-7px.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "NDS10";
|
||||
src: url("/assets/fonts/nds-10px.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* NOTE: woff2 version of this font doesn't work */
|
||||
@font-face {
|
||||
font-family: "Pokemon DP Pro";
|
||||
src: url("/assets/fonts/pokemon-dp-pro.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 102 B |
|
Before Width: | Height: | Size: 438 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 110 B |
|
Before Width: | Height: | Size: 236 B |
|
Before Width: | Height: | Size: 334 B |
|
Before Width: | Height: | Size: 60 B |
|
Before Width: | Height: | Size: 176 B |
|
Before Width: | Height: | Size: 44 B |
|
Before Width: | Height: | Size: 180 B |
|
Before Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 66 B |
|
Before Width: | Height: | Size: 350 B |
|
Before Width: | Height: | Size: 80 B |
|
Before Width: | Height: | Size: 470 B |
|
Before Width: | Height: | Size: 296 B |
|
Before Width: | Height: | Size: 108 B |
|
Before Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 98 B |
|
Before Width: | Height: | Size: 302 B |
|
Before Width: | Height: | Size: 90 B |
|
Before Width: | Height: | Size: 122 B |
|
Before Width: | Height: | Size: 398 B |
|
Before Width: | Height: | Size: 104 B |
|
Before Width: | Height: | Size: 84 B |
|
Before Width: | Height: | Size: 88 B |
|
Before Width: | Height: | Size: 160 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1010 B |
|
Before Width: | Height: | Size: 148 B |
|
Before Width: | Height: | Size: 106 B |
|
Before Width: | Height: | Size: 438 B |
|
Before Width: | Height: | Size: 322 B |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 336 B |
|
Before Width: | Height: | Size: 318 B |
|
Before Width: | Height: | Size: 362 B |
|
Before Width: | Height: | Size: 146 B |
|
Before Width: | Height: | Size: 334 B |
|
Before Width: | Height: | Size: 374 B |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 322 B |
|
Before Width: | Height: | Size: 266 B |
|
Before Width: | Height: | Size: 220 B |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 254 B |
|
Before Width: | Height: | Size: 342 B |
|
Before Width: | Height: | Size: 302 B |
|
Before Width: | Height: | Size: 388 B |
|
Before Width: | Height: | Size: 302 B |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 344 B |
|
Before Width: | Height: | Size: 306 B |
@@ -1,69 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import CORNER_IMAGE from "/assets/images/home/bottom-screen/buttons/corner.webp";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
rect: [x: number, y: number, width: number, height: number];
|
||||
opacity?: number;
|
||||
}>(),
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
);
|
||||
|
||||
const [cornerImage] = useImages(CORNER_IMAGE);
|
||||
|
||||
const ANIMATION_SPEED = 0.25;
|
||||
|
||||
let [currentX, currentY, currentWidth, currentHeight] = props.rect;
|
||||
|
||||
useRender((ctx) => {
|
||||
const [targetX, targetY, targetWidth, targetHeight] = props.rect;
|
||||
const dx = targetX - currentX;
|
||||
const dy = targetY - currentY;
|
||||
const dw = targetWidth - currentWidth;
|
||||
const dh = targetHeight - currentHeight;
|
||||
|
||||
if (
|
||||
Math.abs(dx) < 0.5 &&
|
||||
Math.abs(dy) < 0.5 &&
|
||||
Math.abs(dw) < 0.5 &&
|
||||
Math.abs(dh) < 0.5
|
||||
) {
|
||||
[currentX, currentY, currentWidth, currentHeight] = props.rect;
|
||||
} else {
|
||||
currentX += dx * ANIMATION_SPEED;
|
||||
currentY += dy * ANIMATION_SPEED;
|
||||
currentWidth += dw * ANIMATION_SPEED;
|
||||
currentHeight += dh * ANIMATION_SPEED;
|
||||
}
|
||||
|
||||
const x = Math.floor(currentX);
|
||||
const y = Math.floor(currentY);
|
||||
const w = Math.floor(currentWidth);
|
||||
const h = Math.floor(currentHeight);
|
||||
|
||||
ctx.globalAlpha = props.opacity;
|
||||
|
||||
ctx.drawImage(cornerImage!, x, y);
|
||||
|
||||
ctx.save();
|
||||
ctx.scale(-1, 1);
|
||||
ctx.drawImage(cornerImage!, -(x + w), y);
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.scale(1, -1);
|
||||
ctx.drawImage(cornerImage!, x, -(y + h));
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.scale(-1, -1);
|
||||
ctx.drawImage(cornerImage!, -(x + w), -(y + h));
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,24 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import HOME_BACKGROUND_IMAGE from "/assets/images/home/bottom-screen/background.webp";
|
||||
import CONTACT_BACKGROUND_IMAGE from "/assets/images/contact/bottom-screen/background.webp";
|
||||
|
||||
const store = useContactStore();
|
||||
|
||||
const [homeBackgroundImage, contactBackgroundImage] = useImages(
|
||||
HOME_BACKGROUND_IMAGE,
|
||||
CONTACT_BACKGROUND_IMAGE,
|
||||
);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.drawImage(homeBackgroundImage!, 0, 0);
|
||||
ctx.globalAlpha = store.isIntro
|
||||
? store.intro.stage2Opacity
|
||||
: store.outro.stage3Opacity;
|
||||
|
||||
ctx.drawImage(contactBackgroundImage!, 0, 0);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,39 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import TOP_BAR_IMAGE from "/assets/images/contact/bottom-screen/top-bar.webp";
|
||||
import BOTTOM_BAR_IMAGE from "/assets/images/contact/bottom-screen/bottom-bar.webp";
|
||||
import BOTTOM_BAR_OK_IMAGE from "/assets/images/contact/bottom-screen/ok-button.webp";
|
||||
|
||||
const props = defineProps<{
|
||||
okLabel: "Copy" | "Open";
|
||||
}>();
|
||||
|
||||
const store = useContactStore();
|
||||
|
||||
const [topBarImage, bottomBarImage, bottomBarOkImage] = useImages(
|
||||
TOP_BAR_IMAGE,
|
||||
BOTTOM_BAR_IMAGE,
|
||||
BOTTOM_BAR_OK_IMAGE,
|
||||
);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.globalAlpha = store.isIntro
|
||||
? store.intro.stage3Opacity
|
||||
: store.outro.stage2Opacity;
|
||||
|
||||
// top bar
|
||||
ctx.drawImage(topBarImage!, 0, store.isIntro ? store.intro.topBarY : 0);
|
||||
|
||||
// bottom bar
|
||||
ctx.translate(0, store.isIntro ? store.intro.bottomBarY : SCREEN_HEIGHT - 24);
|
||||
ctx.drawImage(bottomBarImage!, 0, 0);
|
||||
|
||||
ctx.drawImage(bottomBarOkImage!, 144, 4);
|
||||
ctx.font = "10px NDS10";
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText(props.okLabel, 144 + 35, 4 + 13);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,87 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Background from "./Background.vue";
|
||||
import Buttons from "./Buttons.vue";
|
||||
import ButtonSelector from "~/components/Common/ButtonSelector.vue";
|
||||
import Bars from "./Bars.vue";
|
||||
|
||||
const store = useContactStore();
|
||||
|
||||
const ACTIONS = {
|
||||
github: ["Open", "Github profile", "https://github.com/pihkaal"],
|
||||
email: ["Copy", "Email", "hello@pihkaal.me"],
|
||||
website: ["Copy", "Website link", "https://pihkaal.me"],
|
||||
cv: ["Open", "CV", "https://pihkaal.me/cv"],
|
||||
} as const satisfies Record<
|
||||
string,
|
||||
[action: "Copy" | "Open", verb: string, content: string]
|
||||
>;
|
||||
|
||||
const { selectedButton, 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",
|
||||
onButtonClick: async (button) => {
|
||||
actionateButton(button);
|
||||
},
|
||||
});
|
||||
|
||||
const actionateButton = async (button: (typeof selectedButton)["value"]) => {
|
||||
const [action, verb, content] = ACTIONS[button];
|
||||
if (action === "Copy") {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
store.pushNotification(`${verb} copied to clipboard`);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy to clipboard:", error);
|
||||
}
|
||||
} else {
|
||||
await navigateTo(content, { open: { target: "_blank " } });
|
||||
store.pushNotification(`${verb} opened`);
|
||||
}
|
||||
};
|
||||
|
||||
const QUIT_BUTTON: Rect = [31, 172, 80, 18];
|
||||
const OK_BUTTON: Rect = [144, 172, 80, 18];
|
||||
|
||||
useScreenClick((x, y) => {
|
||||
if (rectContains(QUIT_BUTTON, [x, y])) {
|
||||
store.animateOutro();
|
||||
} else if (rectContains(OK_BUTTON, [x, y])) {
|
||||
actionateButton(selectedButton.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Background />
|
||||
|
||||
<Buttons />
|
||||
<ButtonSelector
|
||||
:rect="selectorPosition"
|
||||
:opacity="
|
||||
store.isIntro ? store.intro.stage3Opacity : store.outro.stage1Opacity
|
||||
"
|
||||
/>
|
||||
|
||||
<Bars :ok-label="ACTIONS[selectedButton][0]" />
|
||||
</template>
|
||||
@@ -1,18 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import BUTTONS_IMAGE from "/assets/images/contact/bottom-screen/buttons.webp";
|
||||
|
||||
const store = useContactStore();
|
||||
|
||||
const [buttonsImage] = useImages(BUTTONS_IMAGE);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.globalAlpha = store.isIntro
|
||||
? store.intro.stage3Opacity
|
||||
: store.outro.stage1Opacity;
|
||||
ctx.drawImage(buttonsImage!, 31, 32);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,24 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import HOME_BACKGROUND_IMAGE from "/assets/images/home/top-screen/background.webp";
|
||||
import CONTACT_BACKGROUND_IMAGE from "/assets/images/contact/top-screen/background.webp";
|
||||
|
||||
const store = useContactStore();
|
||||
|
||||
const [homeBackgroundImage, contactBackgroundImage] = useImages(
|
||||
HOME_BACKGROUND_IMAGE,
|
||||
CONTACT_BACKGROUND_IMAGE,
|
||||
);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.drawImage(homeBackgroundImage!, 0, 0);
|
||||
ctx.globalAlpha = store.isIntro
|
||||
? store.intro.stage2Opacity
|
||||
: store.outro.stage3Opacity;
|
||||
|
||||
ctx.drawImage(contactBackgroundImage!, 0, 0);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,27 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import BACKGROUND_IMAGE from "/assets/images/contact/top-screen/left-bar.webp";
|
||||
import THINGS_IMAGE from "/assets/images/contact/top-screen/left-bar-things.webp";
|
||||
|
||||
const store = useContactStore();
|
||||
|
||||
const [backgroundImage, thingsImage] = useImages(
|
||||
BACKGROUND_IMAGE,
|
||||
THINGS_IMAGE,
|
||||
);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.globalAlpha = store.isIntro
|
||||
? store.intro.stage1Opacity
|
||||
: store.outro.stage2Opacity;
|
||||
ctx.drawImage(backgroundImage!, 0, 0);
|
||||
|
||||
ctx.globalAlpha = store.isIntro
|
||||
? store.intro.stage3Opacity
|
||||
: store.outro.stage1Opacity;
|
||||
ctx.drawImage(thingsImage!, 0, 0);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,80 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import NOTIFICATION_IMAGE from "/assets/images/contact/bottom-screen/notification.webp";
|
||||
import TITLE_IMAGE from "/assets/images/contact/top-screen/title.webp";
|
||||
|
||||
// text color:
|
||||
const store = useContactStore();
|
||||
|
||||
const [notificationImage, titleImage] = useImages(
|
||||
NOTIFICATION_IMAGE,
|
||||
TITLE_IMAGE,
|
||||
);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.globalAlpha = store.outro.stage2Opacity;
|
||||
ctx.font = "10px NDS10";
|
||||
|
||||
// notifications
|
||||
for (let i = store.notifications.length - 1; i >= 0; i--) {
|
||||
const index = store.notifications.length - 1 - i;
|
||||
const y = 169 - 24 * index + store.notificationsYOffset;
|
||||
if (y < -24) break;
|
||||
|
||||
ctx.drawImage(notificationImage!, 21, y);
|
||||
|
||||
const content = store.notifications[i]!;
|
||||
ctx.fillStyle = content.includes("opened") ? "#00fbba" : "#e3f300";
|
||||
ctx.fillText(store.notifications[i]!, 27, y + 15);
|
||||
}
|
||||
|
||||
// title
|
||||
ctx.globalAlpha = store.isIntro
|
||||
? store.intro.stage1Opacity
|
||||
: store.outro.stage2Opacity;
|
||||
ctx.drawImage(
|
||||
titleImage!,
|
||||
21,
|
||||
store.isIntro
|
||||
? store.intro.titleY
|
||||
: 169 - 24 * store.notifications.length + store.notificationsYOffset,
|
||||
);
|
||||
|
||||
// notifications count (left bar)
|
||||
const MAX = 36;
|
||||
const MAX_VISIBLE = 8;
|
||||
|
||||
let visibleNotifications = Math.min(store.notifications.length, MAX_VISIBLE);
|
||||
const extraActive =
|
||||
store.notificationsYOffset > 0 && store.notifications.length > MAX_VISIBLE;
|
||||
|
||||
if (extraActive) {
|
||||
visibleNotifications += 1;
|
||||
}
|
||||
|
||||
ctx.fillStyle = "#415969";
|
||||
for (let i = 0; i < visibleNotifications; i++) {
|
||||
ctx.fillRect(3, 161 - i * 4, 12, 2);
|
||||
}
|
||||
|
||||
ctx.fillStyle = "#b2c3db";
|
||||
const startY = 161 - visibleNotifications * 4;
|
||||
const top = MAX - MAX_VISIBLE - (extraActive ? 1 : 0);
|
||||
for (
|
||||
let i = 0;
|
||||
i < store.notifications.length - visibleNotifications && i < top;
|
||||
i++
|
||||
) {
|
||||
if (i === top - 1) {
|
||||
ctx.fillRect(7, startY - i * 4, 4, 2);
|
||||
} else if (i === top - 2) {
|
||||
ctx.fillRect(5, startY - i * 4, 8, 2);
|
||||
} else {
|
||||
ctx.fillRect(3, startY - i * 4, 12, 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Background from "./Background.vue";
|
||||
import LeftBar from "./LeftBar.vue";
|
||||
import Notifications from "./Notifications.vue";
|
||||
|
||||
const store = useContactStore();
|
||||
|
||||
onMounted(() => {
|
||||
store.$reset();
|
||||
store.animateIntro();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Background />
|
||||
|
||||
<LeftBar />
|
||||
|
||||
<Notifications />
|
||||
</template>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import BACKGROUND_IMAGE from "/assets/images/home/bottom-screen/background.webp";
|
||||
|
||||
const store = useHomeStore();
|
||||
const app = useAppStore();
|
||||
|
||||
const [backgroundImage] = useImages(BACKGROUND_IMAGE);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.globalAlpha = app.booted ? 1 : store.intro.stage1Opacity;
|
||||
ctx.drawImage(backgroundImage!, 0, 0);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,10 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Background from "./Background.vue";
|
||||
import Buttons from "./Buttons/Buttons.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Background />
|
||||
|
||||
<Buttons />
|
||||
</template>
|
||||
@@ -1,82 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import GameButton from "./GameButton.vue";
|
||||
import ContactButton from "./ContactButton.vue";
|
||||
import DownloadPlayButton from "./DownloadPlayButton.vue";
|
||||
import SettingsButton from "./SettingsButton.vue";
|
||||
import Selector from "~/components/Common/ButtonSelector.vue";
|
||||
|
||||
const store = useHomeStore();
|
||||
|
||||
const { selectedButton, selectorPosition } = useButtonNavigation({
|
||||
buttons: {
|
||||
projects: [31, 23, 193, 49],
|
||||
contact: [31, 71, 97, 49],
|
||||
downloadPlay: [127, 71, 97, 49],
|
||||
settings: [112, 167, 31, 26],
|
||||
},
|
||||
initialButton: "projects",
|
||||
onButtonClick: (button) => {
|
||||
if (button === "downloadPlay") throw new Error("Not implemented");
|
||||
|
||||
store.animateOutro(button);
|
||||
},
|
||||
navigation: {
|
||||
projects: {
|
||||
down: "last",
|
||||
left: "contact",
|
||||
right: "downloadPlay",
|
||||
horizontalMode: "preview",
|
||||
},
|
||||
contact: {
|
||||
up: "projects",
|
||||
right: "downloadPlay",
|
||||
down: "settings",
|
||||
},
|
||||
downloadPlay: {
|
||||
up: "projects",
|
||||
left: "contact",
|
||||
down: "settings",
|
||||
},
|
||||
settings: {
|
||||
up: "last",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const getButtonOffset = (button: (typeof selectedButton)["value"]) => {
|
||||
if (selectedButton.value === button) return store.outro.buttonOffsetY;
|
||||
return 0;
|
||||
};
|
||||
|
||||
const getOpacity = (button?: (typeof selectedButton)["value"]) => {
|
||||
if (store.isIntro) return store.intro.stage1Opacity;
|
||||
if (selectedButton.value === button) return 1;
|
||||
if (store.isOutro) return store.outro.stage1Opacity;
|
||||
return 1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GameButton
|
||||
:x="33"
|
||||
:y="25 + getButtonOffset('projects')"
|
||||
:opacity="getOpacity('projects')"
|
||||
/>
|
||||
<DownloadPlayButton
|
||||
:x="128"
|
||||
:y="72 + getButtonOffset('downloadPlay')"
|
||||
:opacity="getOpacity('downloadPlay')"
|
||||
/>
|
||||
<ContactButton
|
||||
:x="32"
|
||||
:y="72 + getButtonOffset('contact')"
|
||||
:opacity="getOpacity('contact')"
|
||||
/>
|
||||
<SettingsButton
|
||||
:x="117"
|
||||
:y="170 + getButtonOffset('settings')"
|
||||
:opacity="getOpacity('settings')"
|
||||
/>
|
||||
|
||||
<Selector :rect="selectorPosition" :opacity="getOpacity()" />
|
||||
</template>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import BUTTON_IMAGE from "/assets/images/home/bottom-screen/buttons/contact.webp";
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
opacity: number;
|
||||
}>();
|
||||
|
||||
const [buttonImage] = useImages(BUTTON_IMAGE);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.globalAlpha = props.opacity;
|
||||
ctx.drawImage(buttonImage!, props.x, props.y);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import BUTTON_IMAGE from "/assets/images/home/bottom-screen/buttons/downloadPlay.webp";
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
opacity: number;
|
||||
}>();
|
||||
|
||||
const [buttonImage] = useImages(BUTTON_IMAGE);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.globalAlpha = props.opacity;
|
||||
ctx.drawImage(buttonImage!, props.x, props.y);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import BUTTON_IMAGE from "/assets/images/home/bottom-screen/buttons/game.webp";
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
opacity: number;
|
||||
}>();
|
||||
|
||||
const [buttonImage] = useImages(BUTTON_IMAGE);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.globalAlpha = props.opacity;
|
||||
ctx.drawImage(buttonImage!, props.x, props.y);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import BUTTON_IMAGE from "/assets/images/home/bottom-screen/buttons/settings.webp";
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
opacity: number;
|
||||
}>();
|
||||
|
||||
const [buttonImage] = useImages(BUTTON_IMAGE);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.globalAlpha = props.opacity;
|
||||
ctx.drawImage(buttonImage!, props.x, props.y);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import BACKGROUND_IMAGE from "/assets/images/home/top-screen/background.webp";
|
||||
|
||||
const store = useHomeStore();
|
||||
const app = useAppStore();
|
||||
|
||||
const [backgroundImage] = useImages(BACKGROUND_IMAGE);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.globalAlpha = app.booted ? 1 : store.intro.stage1Opacity;
|
||||
ctx.drawImage(backgroundImage!, 0, 0);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,92 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import CALENDAR_IMAGE from "/assets/images/home/top-screen/calendar/calendar.webp";
|
||||
import LAST_ROW_IMAGE from "/assets/images/home/top-screen/calendar/last-row.webp";
|
||||
import DAY_SELECTOR_IMAGE from "/assets/images/home/top-screen/calendar/day-selector.webp";
|
||||
|
||||
// NOTE: calendar background is handled by TopScreenBackground
|
||||
const store = useHomeStore();
|
||||
|
||||
const [calendarImage, lastRowImage, daySelectorImage] = useImages(
|
||||
CALENDAR_IMAGE,
|
||||
LAST_ROW_IMAGE,
|
||||
DAY_SELECTOR_IMAGE,
|
||||
);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.fillStyle = "black";
|
||||
ctx.font = "7px NDS7";
|
||||
|
||||
const CALENDAR_COLS = 7;
|
||||
const CALENDAR_ROWS = 5;
|
||||
const CALENDAR_LEFT = 128;
|
||||
const CALENDAR_TOP = 64;
|
||||
|
||||
ctx.fillStyle = "#343434";
|
||||
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth();
|
||||
|
||||
const firstDay = new Date(year, month, 1).getDay();
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
|
||||
ctx.globalAlpha = store.isIntro
|
||||
? store.intro.stage1Opacity
|
||||
: store.isOutro && store.outro.animateTop
|
||||
? store.outro.stage1Opacity
|
||||
: 1;
|
||||
ctx.drawImage(calendarImage!, CALENDAR_LEFT - 3, CALENDAR_TOP - 33);
|
||||
|
||||
const extraRow = CALENDAR_COLS * CALENDAR_ROWS - daysInMonth - firstDay < 0;
|
||||
if (extraRow) {
|
||||
ctx.drawImage(lastRowImage!, CALENDAR_LEFT - 3, CALENDAR_TOP + 79);
|
||||
}
|
||||
|
||||
ctx.globalAlpha = store.isIntro
|
||||
? store.intro.stage1Opacity
|
||||
: store.isOutro && store.outro.animateTop
|
||||
? store.outro.stage2Opacity
|
||||
: 1;
|
||||
for (let col = 0; col < CALENDAR_ROWS + (extraRow ? 1 : 0); col += 1) {
|
||||
for (let row = 0; row < CALENDAR_COLS; row += 1) {
|
||||
const cellIndex = col * CALENDAR_COLS + row;
|
||||
const day = cellIndex - firstDay + 1;
|
||||
|
||||
if (day > 0 && day <= daysInMonth) {
|
||||
const dayText = day.toString();
|
||||
const { actualBoundingBoxRight: width } = ctx.measureText(dayText);
|
||||
|
||||
const cellLeft = CALENDAR_LEFT + row * 16;
|
||||
const cellTop = CALENDAR_TOP + col * 16;
|
||||
|
||||
if (now.getDate() === day) {
|
||||
ctx.drawImage(daySelectorImage!, cellLeft, cellTop);
|
||||
}
|
||||
|
||||
ctx.fillText(
|
||||
dayText,
|
||||
cellLeft + Math.floor((15 - width) / 2),
|
||||
cellTop + 11,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = "black";
|
||||
ctx.font = "10px NDS10";
|
||||
ctx.letterSpacing = "2px";
|
||||
|
||||
const timeText = `${month}/${year}`;
|
||||
const { actualBoundingBoxRight: width } = ctx.measureText(timeText);
|
||||
|
||||
ctx.fillText(
|
||||
timeText,
|
||||
CALENDAR_LEFT + Math.floor((111 - width) / 2),
|
||||
CALENDAR_TOP - 20,
|
||||
);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,100 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import CLOCK_IMAGE from "/assets/images/home/top-screen/clock.webp";
|
||||
|
||||
const CENTER_X = 63;
|
||||
const CENTER_Y = 95;
|
||||
|
||||
const store = useHomeStore();
|
||||
|
||||
const [clockImage] = useImages(CLOCK_IMAGE);
|
||||
|
||||
function drawLine(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x0: number,
|
||||
y0: number,
|
||||
x1: number,
|
||||
y1: number,
|
||||
width: number,
|
||||
) {
|
||||
const dx = Math.abs(x1 - x0);
|
||||
const dy = Math.abs(y1 - y0);
|
||||
const sx = x0 < x1 ? 1 : -1;
|
||||
const sy = y0 < y1 ? 1 : -1;
|
||||
let err = dx - dy;
|
||||
|
||||
const drawThickPixel = (x: number, y: number) => {
|
||||
const isVertical = dy > dx;
|
||||
|
||||
if (width === 1) {
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
} else if (isVertical) {
|
||||
const offset = Math.floor((width - 1) / 2);
|
||||
ctx.fillRect(x - offset, y, width, 1);
|
||||
} else {
|
||||
const offset = Math.floor((width - 1) / 2);
|
||||
ctx.fillRect(x, y - offset, 1, width);
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
drawThickPixel(x0, y0);
|
||||
|
||||
if (x0 === x1 && y0 === y1) break;
|
||||
|
||||
const e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
x0 += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
y0 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.globalAlpha = store.isIntro
|
||||
? store.intro.stage1Opacity
|
||||
: store.isOutro && store.outro.animateTop
|
||||
? store.outro.stage1Opacity
|
||||
: 1;
|
||||
ctx.drawImage(clockImage!, 13, 45);
|
||||
|
||||
ctx.globalAlpha = store.isIntro
|
||||
? store.intro.stage1Opacity
|
||||
: store.isOutro && store.outro.animateTop
|
||||
? store.outro.stage2Opacity
|
||||
: 1;
|
||||
const now = new Date();
|
||||
|
||||
const renderHand = (
|
||||
value: number,
|
||||
color: string,
|
||||
length: number,
|
||||
width: number,
|
||||
) => {
|
||||
const angle = value * Math.PI * 2 - Math.PI / 2;
|
||||
const endX = Math.round(CENTER_X + Math.cos(angle) * length);
|
||||
const endY = Math.round(CENTER_Y + Math.sin(angle) * length);
|
||||
ctx.fillStyle = color;
|
||||
drawLine(ctx, CENTER_X, CENTER_Y, endX, endY, width);
|
||||
};
|
||||
|
||||
renderHand(now.getMinutes() / 60, "#797979", 30, 2);
|
||||
renderHand(
|
||||
now.getHours() / 12 + now.getMinutes() / 60 / 12,
|
||||
"#797979",
|
||||
23,
|
||||
2,
|
||||
);
|
||||
renderHand(now.getSeconds() / 60, "#49db8a", 35, 2);
|
||||
|
||||
ctx.fillStyle = "#494949";
|
||||
ctx.fillRect(CENTER_X - 2, CENTER_Y - 2, 5, 5);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,62 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import STATUS_BAR_IMAGE from "/assets/images/home/top-screen/status-bar/status-bar.webp";
|
||||
import GBA_DISPLAY_IMAGE from "/assets/images/home/top-screen/status-bar/gba-display.webp";
|
||||
import STARTUP_MODE_IMAGE from "/assets/images/home/top-screen/status-bar/startup-mode.webp";
|
||||
import BATTERY_IMAGE from "/assets/images/home/top-screen/status-bar/battery.webp";
|
||||
|
||||
const store = useHomeStore();
|
||||
|
||||
const [statusBarImage, gbaDisplayImage, startupModeImage, batteryImage] =
|
||||
useImages(
|
||||
STATUS_BAR_IMAGE,
|
||||
GBA_DISPLAY_IMAGE,
|
||||
STARTUP_MODE_IMAGE,
|
||||
BATTERY_IMAGE,
|
||||
);
|
||||
|
||||
useRender((ctx) => {
|
||||
const TEXT_Y = 11;
|
||||
|
||||
ctx.translate(0, store.isIntro ? store.intro.statusBarY : 0);
|
||||
|
||||
ctx.globalAlpha =
|
||||
store.isOutro && store.outro.animateTop ? store.outro.stage2Opacity : 1;
|
||||
ctx.drawImage(statusBarImage!, 0, 0);
|
||||
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.font = "7px NDS7";
|
||||
|
||||
// username
|
||||
ctx.fillText("pihkaal", 3, TEXT_Y);
|
||||
|
||||
// time + date
|
||||
const fillNumberCell = (value: number, cellX: number, offset: number) => {
|
||||
const text = value.toFixed().padStart(2, "0");
|
||||
const { actualBoundingBoxRight: width } = ctx.measureText(text);
|
||||
|
||||
const x = cellX * 16;
|
||||
ctx.fillText(text, Math.floor(x + offset + (16 - width) / 2), TEXT_Y);
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
|
||||
fillNumberCell(now.getHours(), 9, 1);
|
||||
if (Math.floor(now.getMilliseconds() / 500) % 2 == 0) {
|
||||
ctx.fillText(":", 159, TEXT_Y);
|
||||
}
|
||||
fillNumberCell(now.getMinutes(), 10, -1);
|
||||
|
||||
fillNumberCell(now.getDate(), 11, 1);
|
||||
ctx.fillText("/", 190, TEXT_Y);
|
||||
fillNumberCell(now.getMonth() + 1, 12, -1);
|
||||
|
||||
// icons
|
||||
ctx.drawImage(gbaDisplayImage!, 210, 2);
|
||||
ctx.drawImage(startupModeImage!, 226, 2);
|
||||
ctx.drawImage(batteryImage!, 242, 4);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Background from "./Background.vue";
|
||||
import Calendar from "./Calendar.vue";
|
||||
import Clock from "./Clock.vue";
|
||||
import StatusBar from "./StatusBar.vue";
|
||||
|
||||
const store = useHomeStore();
|
||||
|
||||
onMounted(() => {
|
||||
store.$reset();
|
||||
store.animateIntro();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Background />
|
||||
<Calendar />
|
||||
<Clock />
|
||||
<StatusBar />
|
||||
</template>
|
||||
@@ -1,12 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import BACKGROUND_IMAGE from "~/assets/images/projects/bottom-screen/background.webp";
|
||||
|
||||
const [backgroundImage] = useImages(BACKGROUND_IMAGE);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.drawImage(backgroundImage!, 0, 0);
|
||||
});
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Background from "./Background.vue";
|
||||
import Buttons from "./Buttons.vue";
|
||||
|
||||
const store = useProjectsStore();
|
||||
|
||||
onMounted(async () => {
|
||||
store.$reset();
|
||||
await store.loadProjects();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Background />
|
||||
|
||||
<Buttons v-if="!store.loading" />
|
||||
</template>
|
||||
@@ -1,56 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const store = useProjectsStore();
|
||||
|
||||
const PREV_BUTTON: Point = [36, 100];
|
||||
const QUIT_BUTTON: Point = [88, 156];
|
||||
const LINK_BUTTON: Point = [168, 156];
|
||||
const NEXT_BUTTON: Point = [220, 100];
|
||||
|
||||
const CLICK_RADIUS = 22;
|
||||
|
||||
const circleContains = (
|
||||
[cx, cy]: Point,
|
||||
[x, y]: Point,
|
||||
radius: number,
|
||||
): boolean => Math.sqrt(Math.pow(x - cx, 2) + Math.pow(y - cy, 2)) < radius;
|
||||
|
||||
useScreenClick((x, y) => {
|
||||
const project = store.projects[store.currentProject];
|
||||
if (circleContains(PREV_BUTTON, [x, y], CLICK_RADIUS)) {
|
||||
store.scrollProjects("left");
|
||||
} else if (circleContains(NEXT_BUTTON, [x, y], CLICK_RADIUS)) {
|
||||
store.scrollProjects("right");
|
||||
} else if (circleContains(QUIT_BUTTON, [x, y], CLICK_RADIUS)) {
|
||||
throw new Error("quit");
|
||||
} else if (
|
||||
circleContains(LINK_BUTTON, [x, y], CLICK_RADIUS) &&
|
||||
project?.link
|
||||
) {
|
||||
// TODO: show confirmation popup before opening the link, like "you are about to navigate to [...]"
|
||||
store.visitProject();
|
||||
}
|
||||
});
|
||||
|
||||
useScreenMouseWheel((dy) => {
|
||||
if (dy > 0) {
|
||||
store.scrollProjects("right");
|
||||
} else if (dy < 0) {
|
||||
store.scrollProjects("left");
|
||||
}
|
||||
});
|
||||
|
||||
useKeyDown((key) => {
|
||||
switch (key) {
|
||||
case "ArrowLeft":
|
||||
store.scrollProjects("left");
|
||||
break;
|
||||
case "ArrowRight":
|
||||
store.scrollProjects("right");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,13 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import BACKGROUND_IMAGE from "/assets/images/projects/top-screen/background.webp";
|
||||
|
||||
const [backgroundImage] = useImages(BACKGROUND_IMAGE);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.drawImage(backgroundImage!, 0, 0);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,134 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import BACKGROUND_IMAGE from "~/assets/images/projects/top-screen/background.webp";
|
||||
|
||||
const store = useProjectsStore();
|
||||
|
||||
const [backgroundImage, ...projectImages] = useImages(
|
||||
BACKGROUND_IMAGE,
|
||||
...store.projects.map((project) => `/images/projects/${project.id}.webp`),
|
||||
);
|
||||
|
||||
const drawTextWithShadow = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
color: "white" | "black",
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
) => {
|
||||
ctx.fillStyle = color === "white" ? "#505050" : "#a8b8b8";
|
||||
ctx.fillText(text, x + 1, y + 0);
|
||||
ctx.fillText(text, x + 1, y + 1);
|
||||
ctx.fillText(text, x + 0, y + 1);
|
||||
|
||||
ctx.fillStyle = color === "white" ? "#f8f8f8" : "#101820";
|
||||
ctx.fillText(text, x, y);
|
||||
};
|
||||
|
||||
const drawTextWithShadow2Lines = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
maxWidth: number,
|
||||
line1Color: "white" | "black",
|
||||
line2Color: "white" | "black",
|
||||
) => {
|
||||
const { actualBoundingBoxRight: textWidth } = ctx.measureText(text);
|
||||
|
||||
if (textWidth <= maxWidth) {
|
||||
drawTextWithShadow(ctx, line1Color, text, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
const words = text.split(" ");
|
||||
let firstLine = "";
|
||||
let secondLine = "";
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const testLine = firstLine + (firstLine ? " " : "") + words[i];
|
||||
const { actualBoundingBoxRight: testWidth } = ctx.measureText(testLine);
|
||||
|
||||
if (testWidth > maxWidth && firstLine) {
|
||||
secondLine = words.slice(i).join(" ");
|
||||
break;
|
||||
}
|
||||
firstLine = testLine;
|
||||
}
|
||||
|
||||
drawTextWithShadow(ctx, line1Color, firstLine, x, y);
|
||||
drawTextWithShadow(ctx, line2Color, secondLine, x, y + 16);
|
||||
};
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.drawImage(backgroundImage!, 0, 0);
|
||||
|
||||
ctx.textBaseline = "hanging";
|
||||
ctx.font = "16px Pokemon DP Pro";
|
||||
|
||||
const project = store.projects[store.currentProject];
|
||||
if (!project) return;
|
||||
|
||||
// image
|
||||
const projectImage = projectImages[store.currentProject]!;
|
||||
ctx.drawImage(
|
||||
projectImage,
|
||||
Math.floor(52 - projectImage.width / 2),
|
||||
Math.floor(104 - projectImage.height / 2),
|
||||
);
|
||||
|
||||
// text
|
||||
drawTextWithShadow(ctx, "white", project.title.toUpperCase(), 23, 25);
|
||||
drawTextWithShadow(ctx, "black", project.scope, 12, 41);
|
||||
|
||||
drawTextWithShadow2Lines(
|
||||
ctx,
|
||||
project.description,
|
||||
8,
|
||||
161,
|
||||
90,
|
||||
"white",
|
||||
"black",
|
||||
);
|
||||
|
||||
const { actualBoundingBoxRight: textWidth } = ctx.measureText(
|
||||
project.summary,
|
||||
);
|
||||
drawTextWithShadow(
|
||||
ctx,
|
||||
"black",
|
||||
project.summary,
|
||||
Math.floor(185 - textWidth / 2),
|
||||
19,
|
||||
);
|
||||
|
||||
let textY = 35;
|
||||
for (let i = 0; i < project.tasks.length; i += 1) {
|
||||
const lines = project.tasks[i]!.split("\\n");
|
||||
|
||||
ctx.fillStyle = i % 2 === 0 ? "#6870d8" : "#8890f8";
|
||||
ctx.fillRect(106, textY - 1, 150, lines.length * 16);
|
||||
|
||||
ctx.fillStyle = i % 2 === 0 ? "#8890f8" : "#b0b8d0";
|
||||
ctx.fillRect(105, textY - 1, 1, lines.length * 16);
|
||||
|
||||
for (let j = 0; j < lines.length; j += 1) {
|
||||
drawTextWithShadow(ctx, "white", lines[j]!, 118, textY);
|
||||
textY += 16;
|
||||
}
|
||||
}
|
||||
|
||||
drawTextWithShadow2Lines(
|
||||
ctx,
|
||||
project.technologies.join(", "),
|
||||
111,
|
||||
161,
|
||||
145,
|
||||
"black",
|
||||
"black",
|
||||
);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,12 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Background from "./Background.vue";
|
||||
import Project from "./Project.vue";
|
||||
|
||||
const store = useProjectsStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Background />
|
||||
|
||||
<Project v-if="!store.loading" />
|
||||
</template>
|
||||
@@ -1,127 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
const canvas = useTemplateRef("canvas");
|
||||
|
||||
const updateCallbacks = new Set<UpdateCallback>();
|
||||
const renderCallbacks = new Set<RenderCallback>();
|
||||
const screenClickCallbacks = new Set<ScreenClickCallback>();
|
||||
const screenMouseWheelCallbacks = new Set<ScreenMouseWheelCallback>();
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
let animationFrameId: number | null = null;
|
||||
let lastFrameTime = 0;
|
||||
let lastRealFrameTime = 0;
|
||||
|
||||
const registerUpdateCallback = (callback: UpdateCallback) => {
|
||||
updateCallbacks.add(callback);
|
||||
return () => updateCallbacks.delete(callback);
|
||||
};
|
||||
|
||||
const registerRenderCallback = (callback: RenderCallback) => {
|
||||
renderCallbacks.add(callback);
|
||||
return () => renderCallbacks.delete(callback);
|
||||
};
|
||||
|
||||
const registerScreenClickCallback = (callback: ScreenClickCallback) => {
|
||||
screenClickCallbacks.add(callback);
|
||||
return () => screenClickCallbacks.delete(callback);
|
||||
};
|
||||
|
||||
const registerScreenMouseWheelCallback = (
|
||||
callback: ScreenMouseWheelCallback,
|
||||
) => {
|
||||
screenMouseWheelCallbacks.add(callback);
|
||||
return () => screenMouseWheelCallbacks.delete(callback);
|
||||
};
|
||||
|
||||
const handleCanvasClick = (event: MouseEvent) => {
|
||||
if (!canvas.value) return;
|
||||
|
||||
const rect = canvas.value.getBoundingClientRect();
|
||||
const scaleX = SCREEN_WIDTH / rect.width;
|
||||
const scaleY = SCREEN_HEIGHT / rect.height;
|
||||
|
||||
const x = (event.clientX - rect.left) * scaleX;
|
||||
const y = (event.clientY - rect.top) * scaleY;
|
||||
|
||||
for (const callback of screenClickCallbacks) {
|
||||
callback(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCanvasWheel = (event: WheelEvent) => {
|
||||
for (const callback of screenMouseWheelCallbacks) {
|
||||
callback(event.deltaY, event.deltaX);
|
||||
}
|
||||
};
|
||||
|
||||
const renderFrame = (timestamp: number) => {
|
||||
if (!ctx) return;
|
||||
|
||||
const deltaTime = timestamp - lastFrameTime;
|
||||
lastFrameTime = timestamp;
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
// update
|
||||
for (const callback of updateCallbacks) {
|
||||
callback(deltaTime, lastRealFrameTime);
|
||||
}
|
||||
|
||||
// render
|
||||
ctx.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
|
||||
for (const callback of renderCallbacks) {
|
||||
ctx.save();
|
||||
|
||||
callback(ctx);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
lastRealFrameTime = Date.now() - start;
|
||||
|
||||
animationFrameId = requestAnimationFrame(renderFrame);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!canvas.value) throw new Error("Missing canvas");
|
||||
|
||||
ctx = canvas.value.getContext("2d");
|
||||
if (!ctx) throw new Error("Missing 2d context");
|
||||
|
||||
provide("registerUpdateCallback", registerUpdateCallback);
|
||||
provide("registerRenderCallback", registerRenderCallback);
|
||||
provide("registerScreenClickCallback", registerScreenClickCallback);
|
||||
provide("registerScreenMouseWheelCallback", registerScreenMouseWheelCallback);
|
||||
|
||||
canvas.value.addEventListener("click", handleCanvasClick);
|
||||
canvas.value.addEventListener("wheel", handleCanvasWheel, { passive: true });
|
||||
|
||||
animationFrameId = requestAnimationFrame(renderFrame);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationFrameId !== null) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
|
||||
if (canvas.value) {
|
||||
canvas.value.removeEventListener("click", handleCanvasClick);
|
||||
canvas.value.removeEventListener("wheel", handleCanvasWheel);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<canvas
|
||||
ref="canvas"
|
||||
:width="SCREEN_WIDTH"
|
||||
:height="SCREEN_HEIGHT"
|
||||
:style="{
|
||||
margin: '0',
|
||||
border: '1px solid red',
|
||||
}"
|
||||
/>
|
||||
|
||||
<slot v-if="canvas" />
|
||||
</template>
|
||||
@@ -1,21 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import BACKGROUND_IMAGE from "/assets/images/home/bottom-screen/background.webp";
|
||||
import TOP_BAR_IMAGE from "/assets/images/settings/bottom-screen/top-bar.webp";
|
||||
import BOTTOM_BAR_IMAGE from "/assets/images/settings/bottom-screen/bottom-bar.webp";
|
||||
|
||||
const [backgroundImage, topBarImage, bottomBarImage] = useImages(
|
||||
BACKGROUND_IMAGE,
|
||||
TOP_BAR_IMAGE,
|
||||
BOTTOM_BAR_IMAGE,
|
||||
);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.drawImage(backgroundImage!, 0, 0);
|
||||
ctx.drawImage(topBarImage!, 0, 0);
|
||||
ctx.drawImage(bottomBarImage!, 0, 168);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,9 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Background from "./Background.vue";
|
||||
import Menus from "./Menus/Menus.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Background />
|
||||
<Menus />
|
||||
</template>
|
||||
@@ -1,66 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import MENU_IMAGE from "/assets/images/settings/top-screen/clock/clock.webp";
|
||||
import MENU_ACTIVE_IMAGE from "/assets/images/settings/top-screen/clock/clock-active.webp";
|
||||
import MENU_DISABLED_IMAGE from "/assets/images/settings/top-screen/clock/clock-disabled.png";
|
||||
import ALARM_IMAGE from "/assets/images/settings/top-screen/clock/alarm.webp";
|
||||
import TIME_IMAGE from "/assets/images/settings/top-screen/clock/time.webp";
|
||||
import DATE_IMAGE from "/assets/images/settings/top-screen/clock/date.webp";
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
}>();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const [
|
||||
menuImage,
|
||||
menuActiveImage,
|
||||
menuDisabledImage,
|
||||
alarmImage,
|
||||
timeImage,
|
||||
dateImage,
|
||||
] = useImages(
|
||||
MENU_IMAGE,
|
||||
MENU_ACTIVE_IMAGE,
|
||||
MENU_DISABLED_IMAGE,
|
||||
ALARM_IMAGE,
|
||||
TIME_IMAGE,
|
||||
DATE_IMAGE,
|
||||
);
|
||||
|
||||
const isOpen = computed(() => settingsStore.isMenuOpen("clock"));
|
||||
const isAnyOtherMenuOpen = computed(() =>
|
||||
settingsStore.isAnyOtherMenuOpen("clock"),
|
||||
);
|
||||
|
||||
const animation = useMenuAnimation("clock", isOpen);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.translate(props.x, props.y);
|
||||
|
||||
if (isOpen.value || animation.playing) {
|
||||
ctx.drawImage(
|
||||
timeImage!,
|
||||
48 - animation.stage2Offset,
|
||||
-48 + animation.stage1Offset,
|
||||
);
|
||||
ctx.drawImage(
|
||||
dateImage!,
|
||||
0,
|
||||
-96 + animation.stage2Offset + animation.stage1Offset,
|
||||
);
|
||||
ctx.drawImage(alarmImage!, 0, -48 + animation.stage1Offset);
|
||||
|
||||
ctx.drawImage(menuActiveImage!, 0, 0);
|
||||
} else if (isAnyOtherMenuOpen.value) {
|
||||
ctx.drawImage(menuDisabledImage!, 0, 0);
|
||||
} else {
|
||||
ctx.drawImage(menuImage!, 0, 0);
|
||||
}
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,184 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import OptionsMenu from "./Options/Menu.vue";
|
||||
import OptionsStartUp from "./Options/StartUp.vue";
|
||||
import OptionsLanguage from "./Options/Language.vue";
|
||||
import OptionsGbaMode from "./Options/GbaMode.vue";
|
||||
|
||||
import ClockMenu from "./Clock/Menu.vue";
|
||||
import UserMenu from "./User/Menu.vue";
|
||||
import TouchScreenMenu from "./TouchScreen/Menu.vue";
|
||||
import Selector from "~/components/Common/ButtonSelector.vue";
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const { selectedButton: selected, selectorPosition } = useButtonNavigation({
|
||||
buttons: {
|
||||
options: [31, 119, 49, 49],
|
||||
optionsLanguage: [31, 71, 49, 49],
|
||||
optionsGbaMode: [79, 71, 49, 49],
|
||||
optionsStartUp: [31, 23, 49, 49],
|
||||
|
||||
clock: [79, 119, 49, 49],
|
||||
clockAlarm: [79, 71, 49, 49],
|
||||
clockTime: [127, 71, 49, 49],
|
||||
clockDate: [79, 23, 49, 49],
|
||||
|
||||
user: [127, 119, 49, 49],
|
||||
userBirthday: [79, 71, 49, 49],
|
||||
userName: [127, 71, 49, 49],
|
||||
userMessage: [175, 71, 49, 49],
|
||||
userColor: [127, 23, 49, 49],
|
||||
|
||||
touchScreen: [175, 119, 49, 49],
|
||||
},
|
||||
initialButton: "options",
|
||||
onButtonClick: (buttonName: string) => {
|
||||
if (isSubmenu(buttonName)) {
|
||||
router.push({
|
||||
query: {
|
||||
menu: buttonName,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
navigation: {
|
||||
options: {
|
||||
right: "clock",
|
||||
up: "optionsLanguage",
|
||||
},
|
||||
optionsLanguage: {
|
||||
down: "options",
|
||||
up: "optionsStartUp",
|
||||
right: "optionsGbaMode",
|
||||
},
|
||||
optionsGbaMode: {
|
||||
down: "options",
|
||||
left: "optionsLanguage",
|
||||
up: "optionsStartUp",
|
||||
},
|
||||
optionsStartUp: {
|
||||
right: "optionsGbaMode",
|
||||
down: "optionsLanguage",
|
||||
},
|
||||
|
||||
clock: {
|
||||
left: "options",
|
||||
right: "user",
|
||||
up: "clockAlarm",
|
||||
},
|
||||
clockAlarm: {
|
||||
down: "clock",
|
||||
up: "clockDate",
|
||||
right: "clockTime",
|
||||
},
|
||||
clockTime: {
|
||||
down: "clock",
|
||||
left: "clockAlarm",
|
||||
up: "clockDate",
|
||||
},
|
||||
clockDate: {
|
||||
right: "clockTime",
|
||||
down: "clockAlarm",
|
||||
},
|
||||
|
||||
user: {
|
||||
left: "clock",
|
||||
right: "touchScreen",
|
||||
up: "userName",
|
||||
},
|
||||
userBirthday: {
|
||||
down: "user",
|
||||
up: "userColor",
|
||||
right: "userName",
|
||||
},
|
||||
userName: {
|
||||
down: "user",
|
||||
left: "userBirthday",
|
||||
right: "userMessage",
|
||||
up: "userColor",
|
||||
},
|
||||
userMessage: {
|
||||
down: "user",
|
||||
left: "userName",
|
||||
up: "userColor",
|
||||
},
|
||||
userColor: {
|
||||
left: "userBirthday",
|
||||
right: "userMessage",
|
||||
down: "userName",
|
||||
},
|
||||
|
||||
touchScreen: {
|
||||
left: "user",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const isSubmenu = (buttonName: string) => {
|
||||
return (
|
||||
/^(options|clock|user|touchScreen)[A-Z]/.test(buttonName) &&
|
||||
!["options", "clock", "user", "touchScreen"].includes(buttonName)
|
||||
);
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
onBeforeRouteUpdate((to, from) => {
|
||||
const fromMenu = from.query.menu?.toString();
|
||||
const toMenu = to.query.menu?.toString();
|
||||
|
||||
if (!fromMenu && toMenu) {
|
||||
settingsStore.setActiveMenu(selected.value);
|
||||
} else if (fromMenu && !toMenu) {
|
||||
if (fromMenu === "options" || fromMenu === "clock" || fromMenu === "user") {
|
||||
selected.value = fromMenu;
|
||||
settingsStore.setActiveMenu(null);
|
||||
} else {
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
} else if (fromMenu && toMenu) {
|
||||
if (toMenu === "options" || toMenu === "clock" || toMenu === "user") {
|
||||
settingsStore.setCurrentSubMenu(null);
|
||||
settingsStore.setActiveMenu(selected.value);
|
||||
} else {
|
||||
settingsStore.setCurrentSubMenu(toMenu);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
selected,
|
||||
(newSelected) => {
|
||||
if (settingsStore.currentSubMenu === null) {
|
||||
if (isSubmenu(newSelected)) {
|
||||
router.push({
|
||||
query: {
|
||||
menu: newSelected.split(/[A-Z]/)[0],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
router.push({ query: { menu: undefined } });
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const viewComponents: Record<string, Component> = {
|
||||
optionsStartUp: OptionsStartUp,
|
||||
optionsLanguage: OptionsLanguage,
|
||||
optionsGbaMode: OptionsGbaMode,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="!settingsStore.currentSubMenu">
|
||||
<OptionsMenu :x="33" :y="121" />
|
||||
<ClockMenu :x="81" :y="121" />
|
||||
<UserMenu :x="129" :y="121" />
|
||||
<TouchScreenMenu :x="177" :y="121" :opacity="1" />
|
||||
|
||||
<Selector :rect="selectorPosition" :opacity="1" />
|
||||
</template>
|
||||
<component :is="viewComponents[settingsStore.currentSubMenu]" v-else />
|
||||
</template>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
useRender((ctx) => {
|
||||
ctx.font = "10px NDS10";
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText("GBA Mode", 10, 20);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
useRender((ctx) => {
|
||||
ctx.font = "10px NDS10";
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText("Language", 10, 20);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,66 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import MENU_IMAGE from "/assets/images/settings/top-screen/options/options.webp";
|
||||
import MENU_ACTIVE_IMAGE from "/assets/images/settings/top-screen/options/options-active.png";
|
||||
import MENU_DISABLED_IMAGE from "/assets/images/settings/top-screen/options/options-disabled.png";
|
||||
import GBA_MODE_IMAGE from "/assets/images/settings/top-screen/options/gba-mode.webp";
|
||||
import LANGUAGE_IMAGE from "/assets/images/settings/top-screen/options/language.webp";
|
||||
import START_UP_IMAGE from "/assets/images/settings/top-screen/options/start-up.webp";
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
}>();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const [
|
||||
menuImage,
|
||||
menuActiveImage,
|
||||
menuDisabledImage,
|
||||
gbaModeImage,
|
||||
languageImage,
|
||||
startUpImage,
|
||||
] = useImages(
|
||||
MENU_IMAGE,
|
||||
MENU_ACTIVE_IMAGE,
|
||||
MENU_DISABLED_IMAGE,
|
||||
GBA_MODE_IMAGE,
|
||||
LANGUAGE_IMAGE,
|
||||
START_UP_IMAGE,
|
||||
);
|
||||
|
||||
const isOpen = computed(() => settingsStore.isMenuOpen("options"));
|
||||
const isAnyOtherMenuOpen = computed(() =>
|
||||
settingsStore.isAnyOtherMenuOpen("options"),
|
||||
);
|
||||
|
||||
const animation = useMenuAnimation("options", isOpen);
|
||||
|
||||
useRender((ctx) => {
|
||||
ctx.translate(props.x, props.y);
|
||||
|
||||
if (isOpen.value || animation.playing) {
|
||||
ctx.drawImage(languageImage!, 0, -48 + animation.stage1Offset);
|
||||
ctx.drawImage(
|
||||
gbaModeImage!,
|
||||
48 - animation.stage2Offset,
|
||||
-48 + animation.stage1Offset,
|
||||
);
|
||||
ctx.drawImage(
|
||||
startUpImage!,
|
||||
0,
|
||||
-96 + animation.stage2Offset + animation.stage1Offset,
|
||||
);
|
||||
|
||||
ctx.drawImage(menuActiveImage!, 0, 0);
|
||||
} else if (isAnyOtherMenuOpen.value) {
|
||||
ctx.drawImage(menuDisabledImage!, 0, 0);
|
||||
} else {
|
||||
ctx.drawImage(menuImage!, 0, 0);
|
||||
}
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
useRender((ctx) => {
|
||||
ctx.font = "10px NDS10";
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillText("Startup", 10, 20);
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||
@@ -1,32 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import MENU_IMAGE from "/assets/images/settings/top-screen/touch_screen/touch-screen.webp";
|
||||
import MENU_DISABLED_IMAGE from "/assets/images/settings/top-screen/touch_screen/touch-screen-disabled.png";
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
}>();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const [menuImage, menuDisabledImage] = useImages(
|
||||
MENU_IMAGE,
|
||||
MENU_DISABLED_IMAGE,
|
||||
);
|
||||
|
||||
const isAnyOtherMenuOpen = computed(() =>
|
||||
settingsStore.isAnyOtherMenuOpen("touchScreen"),
|
||||
);
|
||||
|
||||
useRender((ctx) => {
|
||||
if (isAnyOtherMenuOpen.value) {
|
||||
ctx.drawImage(menuDisabledImage!, props.x, props.y);
|
||||
} else {
|
||||
ctx.drawImage(menuImage!, props.x, props.y);
|
||||
}
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
render: () => null,
|
||||
});
|
||||
</script>
|
||||