Compare commits
10 Commits
main
...
108ee082d8
| Author | SHA1 | Date | |
|---|---|---|---|
| 108ee082d8 | |||
| 92f5c83e36 | |||
| 33f918995b | |||
| 88beb5f421 | |||
| 1a82f3c8d0 | |||
| 8d45b76944 | |||
| 285de91dd0 | |||
| 69abfd8aca | |||
| a5dba4b652 | |||
| 1631e3206a |
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
|
OLD
|
||||||
public/images/projects
|
|
||||||
|
|
||||||
# temporary
|
|
||||||
__old
|
|
||||||
|
|
||||||
# ESlint
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Nuxt dev/build outputs
|
|
||||||
.output
|
|
||||||
.data
|
|
||||||
.nuxt
|
|
||||||
.nitro
|
|
||||||
.cache
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Node dependencies
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
# Misc
|
node_modules
|
||||||
.DS_Store
|
dist
|
||||||
.fleet
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
|
.DS_Store
|
||||||
# Local env files
|
*.suo
|
||||||
.env
|
*.ntvs*
|
||||||
.env.*
|
*.njsproj
|
||||||
!.env.example
|
*.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>
|
|
||||||
|
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>
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import MENU_IMAGE from "/assets/images/settings/top-screen/user/user.webp";
|
|
||||||
import MENU_ACTIVE_IMAGE from "/assets/images/settings/top-screen/user/user-active.webp";
|
|
||||||
import MENU_DISABLED_IMAGE from "/assets/images/settings/top-screen/user/user-disabled.png";
|
|
||||||
import BIRTHDAY_IMAGE from "/assets/images/settings/top-screen/user/birthday.webp";
|
|
||||||
import COLOR_IMAGE from "/assets/images/settings/top-screen/user/color.webp";
|
|
||||||
import MESSAGE_IMAGE from "/assets/images/settings/top-screen/user/message.webp";
|
|
||||||
import USER_NAME_IMAGE from "/assets/images/settings/top-screen/user/user-name.webp";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const settingsStore = useSettingsStore();
|
|
||||||
|
|
||||||
const [
|
|
||||||
menuImage,
|
|
||||||
menuActiveImage,
|
|
||||||
menuDisabledImage,
|
|
||||||
birthdayImage,
|
|
||||||
colorImage,
|
|
||||||
messageImage,
|
|
||||||
userNameImage,
|
|
||||||
] = useImages(
|
|
||||||
MENU_IMAGE,
|
|
||||||
MENU_ACTIVE_IMAGE,
|
|
||||||
MENU_DISABLED_IMAGE,
|
|
||||||
BIRTHDAY_IMAGE,
|
|
||||||
COLOR_IMAGE,
|
|
||||||
MESSAGE_IMAGE,
|
|
||||||
USER_NAME_IMAGE,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isOpen = computed(() => settingsStore.isMenuOpen("user"));
|
|
||||||
const isAnyOtherMenuOpen = computed(() =>
|
|
||||||
settingsStore.isAnyOtherMenuOpen("user"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const animation = useMenuAnimation("user", isOpen);
|
|
||||||
|
|
||||||
useRender((ctx) => {
|
|
||||||
ctx.translate(props.x, props.y);
|
|
||||||
|
|
||||||
if (isOpen.value || animation.playing) {
|
|
||||||
ctx.drawImage(
|
|
||||||
birthdayImage!,
|
|
||||||
-48 + animation.stage2Offset,
|
|
||||||
-48 + animation.stage1Offset,
|
|
||||||
);
|
|
||||||
ctx.drawImage(userNameImage!, 0, -48 + animation.stage1Offset);
|
|
||||||
ctx.drawImage(
|
|
||||||
messageImage!,
|
|
||||||
48 - animation.stage2Offset,
|
|
||||||
-48 + animation.stage1Offset,
|
|
||||||
);
|
|
||||||
ctx.drawImage(
|
|
||||||
colorImage!,
|
|
||||||
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,13 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import BACKGROUND_IMAGE from "/assets/images/home/top-screen/background.webp";
|
|
||||||
|
|
||||||
const [backgroundImage] = useImages(BACKGROUND_IMAGE);
|
|
||||||
|
|
||||||
useRender((ctx) => {
|
|
||||||
ctx.drawImage(backgroundImage!, 0, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
render: () => null,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,81 +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";
|
|
||||||
|
|
||||||
const [calendarImage, lastRowImage, daySelectorImage] = useImages(
|
|
||||||
CALENDAR_IMAGE,
|
|
||||||
LAST_ROW_IMAGE,
|
|
||||||
DAY_SELECTOR_IMAGE,
|
|
||||||
);
|
|
||||||
|
|
||||||
useRender((ctx) => {
|
|
||||||
ctx.fillStyle = "black";
|
|
||||||
ctx.font = "7px NDS7";
|
|
||||||
|
|
||||||
ctx.translate(0, -16);
|
|
||||||
|
|
||||||
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.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,90 +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 [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.translate(0, -16);
|
|
||||||
|
|
||||||
ctx.drawImage(clockImage!, 13, 45);
|
|
||||||
|
|
||||||
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>
|
|
||||||