Compare commits
10 Commits
f5090c9d49
...
f6d576dcfc
| Author | SHA1 | Date | |
|---|---|---|---|
| f6d576dcfc | |||
| 9837b5ccdc | |||
| 0c34825482 | |||
| d99410472e | |||
| 515c630a60 | |||
| f6ca995643 | |||
| a5424993b3 | |||
| 161b22259a | |||
| a751c1a150 | |||
| b4b38af4ab |
30
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- beta
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.pihkaal.me
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
run: |
|
||||
docker build -t git.pihkaal.me/pihkaal/pihkaal-me:latest .
|
||||
docker push git.pihkaal.me/pihkaal/pihkaal-me:latest
|
||||
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
|
||||
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM node:22-alpine AS base
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
FROM base AS build
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
FROM base AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/.output ./.output
|
||||
EXPOSE 3000
|
||||
CMD ["node", ".output/server/index.mjs"]
|
||||
21
LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
Copyright (c) 2025 Pihkaal <hello@pihkaal.me>
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
76
README.md
@@ -1,75 +1 @@
|
||||
# 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.
|
||||
# beta.pihkaal.me
|
||||
|
||||
@@ -102,21 +102,26 @@ onRender((ctx) => {
|
||||
);
|
||||
}, 60);
|
||||
|
||||
let lastPressedButton: "A" | "B" | null = null;
|
||||
|
||||
onClick((x, y) => {
|
||||
if (props.yOffset !== 0) return;
|
||||
if (rectContains(B_BUTTON, [x, y])) {
|
||||
if (rectContains(B_BUTTON, [x, y]) && lastPressedButton === "B") {
|
||||
emit("activateB");
|
||||
} else if (rectContains(A_BUTTON, [x, y])) {
|
||||
} else if (rectContains(A_BUTTON, [x, y]) && lastPressedButton === "A") {
|
||||
emit("activateA");
|
||||
}
|
||||
lastPressedButton = null;
|
||||
});
|
||||
|
||||
onMouseDown((x, y) => {
|
||||
if (props.yOffset !== 0) return;
|
||||
if (rectContains(B_BUTTON, [x, y])) {
|
||||
bButtonPressed = true;
|
||||
lastPressedButton = "B";
|
||||
} else if (rectContains(A_BUTTON, [x, y])) {
|
||||
aButtonPressed = true;
|
||||
lastPressedButton = "A";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -68,7 +68,15 @@ const { selected, pressed, selectorPosition } = useButtonNavigation({
|
||||
},
|
||||
},
|
||||
initialButton: "git",
|
||||
onActivate: async (button) => {
|
||||
onActivate: (button) => handleActivateA(button),
|
||||
disabled: computed(() => store.isIntro || store.isOutro),
|
||||
selectorAnimation: {
|
||||
duration: 0.075,
|
||||
ease: "none",
|
||||
},
|
||||
});
|
||||
|
||||
const handleActivateA = async (button: typeof selected.value) => {
|
||||
const { action, url } = BUTTONS[button];
|
||||
const verb = $t(`contact.actions.${button}`);
|
||||
if (action === "copy") {
|
||||
@@ -94,13 +102,7 @@ const { selected, pressed, selectorPosition } = useButtonNavigation({
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
disabled: computed(() => store.isIntro || store.isOutro),
|
||||
selectorAnimation: {
|
||||
duration: 0.075,
|
||||
ease: "none",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleActivateB = () => {
|
||||
if (store.isIntro || store.isOutro) return;
|
||||
@@ -157,6 +159,7 @@ onRender((ctx) => {
|
||||
"
|
||||
:b-label="$t('common.quit')"
|
||||
:a-label="$t(`contact.actions.${BUTTONS[selected].action}`)"
|
||||
@activate-a="handleActivateA(selected)"
|
||||
@activate-b="handleActivateB"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -274,51 +274,103 @@ const pressButton = (button: string) => {
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", { key: `NDS_${button}` }));
|
||||
};
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (!hasAnimated.value) {
|
||||
animateIntro();
|
||||
return;
|
||||
}
|
||||
|
||||
const raycast = (clientX: number, clientY: number) => {
|
||||
const domElement = renderer.instance.domElement;
|
||||
const rect = domElement.getBoundingClientRect();
|
||||
|
||||
const raycaster = new THREE.Raycaster();
|
||||
raycaster.setFromCamera(
|
||||
new THREE.Vector2(
|
||||
((event.clientX - rect.left) / rect.width) * 2 - 1,
|
||||
-((event.clientY - rect.top) / rect.height) * 2 + 1,
|
||||
((clientX - rect.left) / rect.width) * 2 - 1,
|
||||
-((clientY - rect.top) / rect.height) * 2 + 1,
|
||||
),
|
||||
camera.activeCamera.value,
|
||||
);
|
||||
|
||||
const intersects = raycaster.intersectObjects(model.children, true);
|
||||
const intersection = intersects[0];
|
||||
if (!intersection?.uv) return;
|
||||
return intersects[0];
|
||||
};
|
||||
|
||||
switch (intersection.object.name) {
|
||||
case TOP_SCREEN:
|
||||
case BOTTOM_SCREEN: {
|
||||
const canvas =
|
||||
intersection.object.name === TOP_SCREEN
|
||||
? props.topScreenCanvas
|
||||
: props.bottomScreenCanvas;
|
||||
const getScreenCanvas = (name: string) => {
|
||||
if (name === TOP_SCREEN) return props.topScreenCanvas;
|
||||
if (name === BOTTOM_SCREEN) return props.bottomScreenCanvas;
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!canvas) break;
|
||||
const toScreenCoords = (
|
||||
canvas: HTMLCanvasElement,
|
||||
intersection: THREE.Intersection,
|
||||
) => {
|
||||
if (!intersection.uv) return null;
|
||||
|
||||
const logicalX = (1 - intersection.uv.x) * LOGICAL_WIDTH;
|
||||
const logicalY = (1 - intersection.uv.y) * LOGICAL_HEIGHT;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.dispatchEvent(
|
||||
new MouseEvent("click", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
return {
|
||||
clientX: (logicalX / LOGICAL_WIDTH) * rect.width + rect.left,
|
||||
clientY: (logicalY / LOGICAL_HEIGHT) * rect.height + rect.top,
|
||||
};
|
||||
};
|
||||
|
||||
const dispatchScreenEvent = (
|
||||
type: string,
|
||||
canvas: HTMLCanvasElement,
|
||||
intersection: THREE.Intersection,
|
||||
) => {
|
||||
const coords = toScreenCoords(canvas, intersection);
|
||||
if (!coords) return;
|
||||
|
||||
canvas.dispatchEvent(
|
||||
new MouseEvent(type, { bubbles: true, cancelable: true, ...coords }),
|
||||
);
|
||||
};
|
||||
|
||||
const dispatchScreenTouchEvent = (
|
||||
type: string,
|
||||
canvas: HTMLCanvasElement,
|
||||
intersection: THREE.Intersection,
|
||||
) => {
|
||||
const coords = toScreenCoords(canvas, intersection);
|
||||
if (!coords) return;
|
||||
|
||||
const touch = new Touch({ identifier: 0, target: canvas, ...coords });
|
||||
|
||||
canvas.dispatchEvent(
|
||||
new TouchEvent(type, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
touches: type === "touchend" ? [] : [touch],
|
||||
changedTouches: [touch],
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const BUTTON_MAP = {
|
||||
[X_BUTTON]: "X",
|
||||
[A_BUTTON]: "A",
|
||||
[Y_BUTTON]: "Y",
|
||||
[B_BUTTON]: "B",
|
||||
[SELECT_BUTTON]: "SELECT",
|
||||
[START_BUTTON]: "START",
|
||||
} as const;
|
||||
|
||||
const handleInteraction = (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
onScreen: (
|
||||
canvas: HTMLCanvasElement,
|
||||
intersection: THREE.Intersection,
|
||||
) => void,
|
||||
) => {
|
||||
const intersection = raycast(clientX, clientY);
|
||||
if (!intersection?.uv) return;
|
||||
|
||||
switch (intersection.object.name) {
|
||||
case TOP_SCREEN:
|
||||
case BOTTOM_SCREEN: {
|
||||
const canvas = getScreenCanvas(intersection.object.name);
|
||||
if (canvas) onScreen(canvas, intersection);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -339,37 +391,84 @@ const handleClick = (event: MouseEvent) => {
|
||||
break;
|
||||
}
|
||||
|
||||
case X_BUTTON:
|
||||
case A_BUTTON:
|
||||
case Y_BUTTON:
|
||||
case B_BUTTON:
|
||||
case SELECT_BUTTON:
|
||||
case START_BUTTON: {
|
||||
const BUTTON_MAP = {
|
||||
[X_BUTTON]: "X",
|
||||
[A_BUTTON]: "A",
|
||||
[Y_BUTTON]: "Y",
|
||||
[B_BUTTON]: "B",
|
||||
[SELECT_BUTTON]: "SELECT",
|
||||
[START_BUTTON]: "START",
|
||||
} as const;
|
||||
|
||||
const button = BUTTON_MAP[intersection.object.name];
|
||||
default: {
|
||||
const button =
|
||||
BUTTON_MAP[intersection.object.name as keyof typeof BUTTON_MAP];
|
||||
if (button) pressButton(button);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (mousePressedButton) {
|
||||
physicalButtonsDown.delete(mousePressedButton);
|
||||
const releaseButton = () => {
|
||||
if (!mousePressedButton) return;
|
||||
|
||||
physicalButtonsDown.delete(mousePressedButton);
|
||||
window.dispatchEvent(
|
||||
new KeyboardEvent("keyup", { key: `NDS_${mousePressedButton}` }),
|
||||
);
|
||||
|
||||
mousePressedButton = null;
|
||||
};
|
||||
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
if (!hasAnimated.value) {
|
||||
animateIntro();
|
||||
return;
|
||||
}
|
||||
|
||||
handleInteraction(event.clientX, event.clientY, (canvas, intersection) => {
|
||||
dispatchScreenEvent("mousedown", canvas, intersection);
|
||||
});
|
||||
};
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (!hasAnimated.value) return;
|
||||
|
||||
const intersection = raycast(event.clientX, event.clientY);
|
||||
if (!intersection?.uv) return;
|
||||
|
||||
const canvas = getScreenCanvas(intersection.object.name);
|
||||
if (canvas) dispatchScreenEvent("click", canvas, intersection);
|
||||
};
|
||||
|
||||
const handleMouseUp = (event: MouseEvent) => {
|
||||
releaseButton();
|
||||
|
||||
const intersection = raycast(event.clientX, event.clientY);
|
||||
if (!intersection?.uv) return;
|
||||
|
||||
const canvas = getScreenCanvas(intersection.object.name);
|
||||
if (canvas) dispatchScreenEvent("mouseup", canvas, intersection);
|
||||
};
|
||||
|
||||
const handleTouchStart = (event: TouchEvent) => {
|
||||
const touch = event.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
if (!hasAnimated.value) {
|
||||
animateIntro();
|
||||
return;
|
||||
}
|
||||
|
||||
handleInteraction(touch.clientX, touch.clientY, (canvas, intersection) => {
|
||||
dispatchScreenEvent("mousedown", canvas, intersection);
|
||||
dispatchScreenTouchEvent("touchstart", canvas, intersection);
|
||||
});
|
||||
};
|
||||
|
||||
const handleTouchEnd = (event: TouchEvent) => {
|
||||
releaseButton();
|
||||
|
||||
const touch = event.changedTouches[0];
|
||||
if (!touch) return;
|
||||
|
||||
const intersection = raycast(touch.clientX, touch.clientY);
|
||||
if (!intersection?.uv) return;
|
||||
|
||||
const canvas = getScreenCanvas(intersection.object.name);
|
||||
if (canvas) {
|
||||
dispatchScreenEvent("click", canvas, intersection);
|
||||
dispatchScreenTouchEvent("touchend", canvas, intersection);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -377,15 +476,33 @@ onMounted(() => {
|
||||
app.ready = true;
|
||||
|
||||
if (renderer) {
|
||||
renderer.instance.domElement.addEventListener("mousedown", handleClick);
|
||||
renderer.instance.domElement.addEventListener("mousedown", handleMouseDown);
|
||||
renderer.instance.domElement.addEventListener("click", handleClick);
|
||||
renderer.instance.domElement.addEventListener("mouseup", handleMouseUp);
|
||||
renderer.instance.domElement.addEventListener(
|
||||
"touchstart",
|
||||
handleTouchStart,
|
||||
);
|
||||
renderer.instance.domElement.addEventListener("touchend", handleTouchEnd);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (renderer) {
|
||||
renderer.instance.domElement.removeEventListener("mousedown", handleClick);
|
||||
renderer.instance.domElement.removeEventListener(
|
||||
"mousedown",
|
||||
handleMouseDown,
|
||||
);
|
||||
renderer.instance.domElement.removeEventListener("click", handleClick);
|
||||
renderer.instance.domElement.removeEventListener("mouseup", handleMouseUp);
|
||||
renderer.instance.domElement.removeEventListener(
|
||||
"touchstart",
|
||||
handleTouchStart,
|
||||
);
|
||||
renderer.instance.domElement.removeEventListener(
|
||||
"touchend",
|
||||
handleTouchEnd,
|
||||
);
|
||||
}
|
||||
topScreenTexture?.dispose();
|
||||
bottomScreenTexture?.dispose();
|
||||
|
||||
@@ -96,14 +96,29 @@ const dispatchSwipe = (endX: number, endY: number) => {
|
||||
const handleTouchStart = (event: TouchEvent) => {
|
||||
const touch = event.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
swipeStartX = touch.clientX;
|
||||
swipeStartY = touch.clientY;
|
||||
|
||||
canvas.value?.dispatchEvent(
|
||||
new MouseEvent("mousedown", {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleTouchEnd = (event: TouchEvent) => {
|
||||
const touch = event.changedTouches[0];
|
||||
if (!touch) return;
|
||||
|
||||
dispatchSwipe(touch.clientX, touch.clientY);
|
||||
document.dispatchEvent(
|
||||
new MouseEvent("mouseup", {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
let mouseSwiping = false;
|
||||
@@ -178,7 +193,7 @@ onMounted(() => {
|
||||
});
|
||||
canvas.value.addEventListener("touchend", handleTouchEnd, { passive: true });
|
||||
canvas.value.addEventListener("mousedown", handleSwipeMouseDown);
|
||||
document.addEventListener("mouseup", handleSwipeMouseUp);
|
||||
canvas.value.addEventListener("mouseup", handleSwipeMouseUp);
|
||||
|
||||
animationFrameId = requestAnimationFrame(renderFrame);
|
||||
});
|
||||
@@ -195,8 +210,8 @@ onUnmounted(() => {
|
||||
canvas.value.removeEventListener("touchstart", handleTouchStart);
|
||||
canvas.value.removeEventListener("touchend", handleTouchEnd);
|
||||
canvas.value.removeEventListener("mousedown", handleSwipeMouseDown);
|
||||
canvas.value.removeEventListener("mouseup", handleSwipeMouseUp);
|
||||
}
|
||||
document.removeEventListener("mouseup", handleSwipeMouseUp);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -35,6 +35,9 @@ const confirmationModal = useConfirmationModal();
|
||||
|
||||
const isAnimating = ref(true);
|
||||
|
||||
const bLabel = ref($t("common.goBack"));
|
||||
const aLabel = ref($t("common.select"));
|
||||
|
||||
const SLIDE_OFFSET = 96;
|
||||
const SLIDE_DURATION = 0.25;
|
||||
const ARROW_SLIDE_DELAY = 0.15;
|
||||
@@ -60,6 +63,14 @@ const animateIntro = async () => {
|
||||
.timeline()
|
||||
.to(animation, { offsetY: 0, duration: SLIDE_DURATION, ease: "none" }, 0)
|
||||
.to(animation, { opacity: 1, duration: SLIDE_DURATION, ease: "none" }, 0)
|
||||
.call(
|
||||
() => {
|
||||
bLabel.value = $t("common.cancel");
|
||||
aLabel.value = $t("common.reset");
|
||||
},
|
||||
[],
|
||||
SUBMENU_LABEL_CHANGE_DELAY,
|
||||
)
|
||||
.to(
|
||||
animation,
|
||||
{ viewAllOffsetY: 0, duration: ARROW_SLIDE_DURATION, ease: "none" },
|
||||
@@ -197,8 +208,8 @@ onRender((ctx) => {
|
||||
|
||||
<CommonButtons
|
||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||
:b-label="$t('common.cancel')"
|
||||
:a-label="$t('common.reset')"
|
||||
:b-label="bLabel"
|
||||
:a-label="aLabel"
|
||||
@activate-b="handleActivateB()"
|
||||
@activate-a="handleReset()"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { SettingsBottomScreenNumberInput as NumberInput } from "#components";
|
||||
import { useIntervalFn } from "@vueuse/core";
|
||||
import gsap from "gsap";
|
||||
|
||||
const store = useSettingsStore();
|
||||
|
||||
@@ -12,6 +13,9 @@ useIntervalFn(() => {
|
||||
|
||||
const isAnimating = ref(true);
|
||||
|
||||
const bLabel = ref($t("common.goBack"));
|
||||
const aLabel = ref($t("common.select"));
|
||||
|
||||
const monthRef = useTemplateRef<InstanceType<typeof NumberInput>>("month");
|
||||
const dayRef = useTemplateRef<InstanceType<typeof NumberInput>>("day");
|
||||
const yearRef = useTemplateRef<InstanceType<typeof NumberInput>>("year");
|
||||
@@ -22,6 +26,14 @@ const animateIntro = async () => {
|
||||
monthRef.value?.animateIntro(),
|
||||
dayRef.value?.animateIntro(),
|
||||
yearRef.value?.animateIntro(),
|
||||
gsap.timeline().call(
|
||||
() => {
|
||||
bLabel.value = $t("common.cancel");
|
||||
aLabel.value = $t("common.confirm");
|
||||
},
|
||||
[],
|
||||
SUBMENU_LABEL_CHANGE_DELAY,
|
||||
),
|
||||
]);
|
||||
isAnimating.value = false;
|
||||
};
|
||||
@@ -80,8 +92,8 @@ defineOptions({ render: () => null });
|
||||
|
||||
<CommonButtons
|
||||
:y-offset="store.submenuButtonsOffsetY"
|
||||
:b-label="$t('common.cancel')"
|
||||
:a-label="$t('common.confirm')"
|
||||
:b-label="bLabel"
|
||||
:a-label="aLabel"
|
||||
@activate-b="handleActivateB"
|
||||
@activate-a="handleActivateA"
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,9 @@ const animation = reactive({
|
||||
|
||||
const isAnimating = ref(true);
|
||||
|
||||
const bLabel = ref($t("common.goBack"));
|
||||
const aLabel = ref($t("common.select"));
|
||||
|
||||
const hourRef = useTemplateRef<InstanceType<typeof NumberInput>>("hour");
|
||||
const minuteRef = useTemplateRef<InstanceType<typeof NumberInput>>("minute");
|
||||
|
||||
@@ -34,7 +37,15 @@ const animateIntro = async () => {
|
||||
gsap
|
||||
.timeline()
|
||||
.to(animation, { offsetY: 0, duration: SLIDE_DURATION, ease: "none" }, 0)
|
||||
.to(animation, { opacity: 1, duration: SLIDE_DURATION, ease: "none" }, 0),
|
||||
.to(animation, { opacity: 1, duration: SLIDE_DURATION, ease: "none" }, 0)
|
||||
.call(
|
||||
() => {
|
||||
bLabel.value = $t("common.cancel");
|
||||
aLabel.value = $t("common.confirm");
|
||||
},
|
||||
[],
|
||||
SUBMENU_LABEL_CHANGE_DELAY,
|
||||
),
|
||||
]);
|
||||
isAnimating.value = false;
|
||||
};
|
||||
@@ -98,8 +109,8 @@ defineOptions({ render: () => null });
|
||||
|
||||
<CommonButtons
|
||||
:y-offset="store.submenuButtonsOffsetY"
|
||||
:b-label="$t('common.cancel')"
|
||||
:a-label="$t('common.confirm')"
|
||||
:b-label="bLabel"
|
||||
:a-label="aLabel"
|
||||
@activate-b="handleActivateB"
|
||||
@activate-a="handleActivateA"
|
||||
/>
|
||||
|
||||
@@ -250,6 +250,21 @@ const selectorTransitionOffsetY = computed(() => {
|
||||
return store.submenuTransition.offsetY;
|
||||
});
|
||||
|
||||
const handleActivateA = () => {
|
||||
if (store.isIntro || store.isOutro || store.submenuTransition.opacity < 1)
|
||||
return;
|
||||
|
||||
if (isSubMenu(selected.value)) {
|
||||
store.openSubMenu(selected.value);
|
||||
} else {
|
||||
if (selected.value === "options") select("optionsLanguage");
|
||||
if (selected.value === "clock") select("clockAchievements");
|
||||
if (selected.value === "user") select("userUserName");
|
||||
if (selected.value === "touchScreen")
|
||||
store.openSubMenu("touchScreenTapTap");
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivateB = () => {
|
||||
if (store.isIntro || store.isOutro || store.submenuTransition.opacity < 1)
|
||||
return;
|
||||
@@ -318,6 +333,7 @@ const handleActivateB = () => {
|
||||
:y-offset="store.barOffsetY + store.submenuButtonsOffsetY"
|
||||
:b-label="isSubmenuSelected ? $t('common.goBack') : $t('common.quit')"
|
||||
:a-label="$t('common.select')"
|
||||
@activate-a="handleActivateA()"
|
||||
@activate-b="handleActivateB()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -77,6 +77,9 @@ const SCORE_DURATION = 0.167;
|
||||
|
||||
const isAnimating = ref(true);
|
||||
|
||||
const bLabel = ref($t("common.goBack"));
|
||||
const aLabel = ref($t("common.select"));
|
||||
|
||||
const intro = reactive({
|
||||
frameOffsetY: SLIDE_OFFSET,
|
||||
frameOpacity: 0,
|
||||
@@ -148,7 +151,11 @@ const animateIntro = async () => {
|
||||
buildTilesFromBoard();
|
||||
|
||||
await gsap
|
||||
.timeline()
|
||||
.timeline({
|
||||
onComplete: () => {
|
||||
isAnimating.value = false;
|
||||
},
|
||||
})
|
||||
.to(intro, { frameOffsetY: 0, duration: SLIDE_DURATION, ease: "none" }, 0)
|
||||
.to(intro, { frameOpacity: 1, duration: SLIDE_DURATION, ease: "none" }, 0)
|
||||
.call(
|
||||
@@ -163,14 +170,25 @@ const animateIntro = async () => {
|
||||
intro,
|
||||
{ scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
|
||||
SLIDE_DURATION,
|
||||
)
|
||||
.call(
|
||||
() => {
|
||||
bLabel.value = $t("common.quit");
|
||||
aLabel.value = $t("common.restart");
|
||||
},
|
||||
[],
|
||||
SUBMENU_LABEL_CHANGE_DELAY,
|
||||
);
|
||||
isAnimating.value = false;
|
||||
};
|
||||
|
||||
const animateOutro = async () => {
|
||||
isAnimating.value = true;
|
||||
await gsap
|
||||
.timeline()
|
||||
.timeline({
|
||||
onComplete: () => {
|
||||
isAnimating.value = false;
|
||||
},
|
||||
})
|
||||
.to(
|
||||
intro,
|
||||
{ frameOffsetY: SLIDE_OFFSET, duration: SLIDE_DURATION, ease: "none" },
|
||||
@@ -581,8 +599,8 @@ useKeyDown(({ key }) => {
|
||||
<template>
|
||||
<CommonButtons
|
||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||
:b-label="$t('common.quit')"
|
||||
:a-label="$t('common.restart')"
|
||||
:b-label="bLabel"
|
||||
:a-label="aLabel"
|
||||
@activate-b="handleActivateB()"
|
||||
@activate-a="handleActivateA()"
|
||||
/>
|
||||
|
||||
@@ -28,6 +28,9 @@ const BUTTON_POSITIONS = [
|
||||
|
||||
const isAnimating = ref(true);
|
||||
|
||||
const bLabel = ref($t("common.goBack"));
|
||||
const aLabel = ref($t("common.select"));
|
||||
|
||||
const { selected, selectorPosition } = useButtonNavigation({
|
||||
buttons: {
|
||||
english: [10, 27, 106, 41],
|
||||
@@ -95,7 +98,11 @@ const animation = reactive({
|
||||
|
||||
const animateIntro = async () => {
|
||||
isAnimating.value = true;
|
||||
const timeline = gsap.timeline();
|
||||
const timeline = gsap.timeline({
|
||||
onComplete: () => {
|
||||
isAnimating.value = false;
|
||||
},
|
||||
});
|
||||
for (let i = 0; i < ROW_COUNT; i++) {
|
||||
timeline
|
||||
.to(
|
||||
@@ -109,13 +116,24 @@ const animateIntro = async () => {
|
||||
i * ROW_STAGGER,
|
||||
);
|
||||
}
|
||||
timeline.call(
|
||||
() => {
|
||||
bLabel.value = $t("common.cancel");
|
||||
aLabel.value = $t("common.confirm");
|
||||
},
|
||||
[],
|
||||
SUBMENU_LABEL_CHANGE_DELAY,
|
||||
);
|
||||
await timeline;
|
||||
isAnimating.value = false;
|
||||
};
|
||||
|
||||
const animateOutro = async () => {
|
||||
isAnimating.value = true;
|
||||
const timeline = gsap.timeline();
|
||||
const timeline = gsap.timeline({
|
||||
onComplete: () => {
|
||||
isAnimating.value = false;
|
||||
},
|
||||
});
|
||||
for (let i = 0; i < ROW_COUNT; i++) {
|
||||
timeline
|
||||
.to(
|
||||
@@ -205,8 +223,9 @@ defineOptions({
|
||||
<template>
|
||||
<CommonButtons
|
||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||
:b-label="$t('common.cancel')"
|
||||
:a-label="$t('common.confirm')"
|
||||
:b-label="bLabel"
|
||||
:a-label="aLabel"
|
||||
@activate-a="handleActivateA"
|
||||
@activate-b="handleActivateB"
|
||||
/>
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ const SLIDE_DURATION = 0.25;
|
||||
|
||||
const isAnimating = ref(true);
|
||||
|
||||
const bLabel = ref($t("common.goBack"));
|
||||
const aLabel = ref($t("common.select"));
|
||||
|
||||
const animation = reactive({
|
||||
_3dMode: { headerOffsetY: HEADER_HEIGHT * 3, opacity: 0 },
|
||||
_2dMode: { headerOffsetY: HEADER_HEIGHT * 3, opacity: 0 },
|
||||
@@ -48,6 +51,14 @@ const animateIntro = async () => {
|
||||
ease: "none",
|
||||
},
|
||||
BUTTON_STAGGER,
|
||||
)
|
||||
.call(
|
||||
() => {
|
||||
bLabel.value = $t("common.cancel");
|
||||
aLabel.value = $t("common.confirm");
|
||||
},
|
||||
[],
|
||||
SUBMENU_LABEL_CHANGE_DELAY,
|
||||
);
|
||||
isAnimating.value = false;
|
||||
};
|
||||
@@ -183,8 +194,9 @@ onRender((ctx) => {
|
||||
<template>
|
||||
<CommonButtons
|
||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||
:b-label="$t('common.cancel')"
|
||||
:a-label="$t('common.confirm')"
|
||||
:b-label="bLabel"
|
||||
:a-label="aLabel"
|
||||
@activate-a="handleActivateA"
|
||||
@activate-b="handleActivateB"
|
||||
/>
|
||||
|
||||
|
||||
@@ -68,6 +68,17 @@ let isNewBest = false;
|
||||
|
||||
const isAnimating = ref(true);
|
||||
|
||||
const labelsReady = ref(false);
|
||||
const bLabel = $t("common.quit");
|
||||
const aLabel = computed(() => {
|
||||
if (!labelsReady.value) return $t("common.select");
|
||||
return state.value === "waiting" ? $t("common.start") : $t("common.restart");
|
||||
});
|
||||
|
||||
const buttonsRef = useTemplateRef<{ forceAnimateBLabel: () => void }>(
|
||||
"buttons",
|
||||
);
|
||||
|
||||
const AREA_FADE_DURATION = 0.2;
|
||||
const SCORE_OFFSET = -20;
|
||||
const SCORE_DURATION = 0.167;
|
||||
@@ -81,7 +92,11 @@ const animation = reactive({
|
||||
const animateIntro = async () => {
|
||||
isAnimating.value = true;
|
||||
await gsap
|
||||
.timeline()
|
||||
.timeline({
|
||||
onComplete: () => {
|
||||
isAnimating.value = false;
|
||||
},
|
||||
})
|
||||
.to(
|
||||
animation,
|
||||
{ areaOpacity: 1, duration: AREA_FADE_DURATION, ease: "none" },
|
||||
@@ -91,8 +106,15 @@ const animateIntro = async () => {
|
||||
animation,
|
||||
{ scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
|
||||
AREA_FADE_DURATION,
|
||||
)
|
||||
.call(
|
||||
() => {
|
||||
labelsReady.value = true;
|
||||
buttonsRef.value?.forceAnimateBLabel();
|
||||
},
|
||||
[],
|
||||
SUBMENU_LABEL_CHANGE_DELAY,
|
||||
);
|
||||
isAnimating.value = false;
|
||||
};
|
||||
|
||||
const animateOutro = async () => {
|
||||
@@ -101,7 +123,11 @@ const animateOutro = async () => {
|
||||
targetY = LOGICAL_HEIGHT * 2 - 20;
|
||||
|
||||
await gsap
|
||||
.timeline()
|
||||
.timeline({
|
||||
onComplete: () => {
|
||||
isAnimating.value = false;
|
||||
},
|
||||
})
|
||||
.to(
|
||||
animation,
|
||||
{ areaOpacity: 0, duration: AREA_FADE_DURATION, ease: "none" },
|
||||
@@ -418,9 +444,10 @@ defineOptions({ render: () => null });
|
||||
|
||||
<template>
|
||||
<CommonButtons
|
||||
ref="buttons"
|
||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||
:b-label="$t('common.quit')"
|
||||
:a-label="state === 'waiting' ? $t('common.start') : $t('common.restart')"
|
||||
:b-label="bLabel"
|
||||
:a-label="aLabel"
|
||||
@activate-b="handleActivateB"
|
||||
@activate-a="handleActivateA"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { SettingsBottomScreenNumberInput as NumberInput } from "#components";
|
||||
import gsap from "gsap";
|
||||
|
||||
const store = useSettingsStore();
|
||||
const confirmationModal = useConfirmationModal();
|
||||
@@ -10,6 +11,9 @@ const BIRTHDAY_YEAR = 2002;
|
||||
|
||||
const isAnimating = ref(true);
|
||||
|
||||
const bLabel = ref($t("common.goBack"));
|
||||
const aLabel = ref($t("common.select"));
|
||||
|
||||
const monthRef = useTemplateRef<InstanceType<typeof NumberInput>>("month");
|
||||
const dayRef = useTemplateRef<InstanceType<typeof NumberInput>>("day");
|
||||
const yearRef = useTemplateRef<InstanceType<typeof NumberInput>>("year");
|
||||
@@ -20,6 +24,14 @@ const animateIntro = async () => {
|
||||
monthRef.value?.animateIntro(),
|
||||
dayRef.value?.animateIntro(),
|
||||
yearRef.value?.animateIntro(),
|
||||
gsap.timeline().call(
|
||||
() => {
|
||||
bLabel.value = $t("common.cancel");
|
||||
aLabel.value = $t("common.confirm");
|
||||
},
|
||||
[],
|
||||
SUBMENU_LABEL_CHANGE_DELAY,
|
||||
),
|
||||
]);
|
||||
isAnimating.value = false;
|
||||
};
|
||||
@@ -113,8 +125,8 @@ defineOptions({ render: () => null });
|
||||
|
||||
<CommonButtons
|
||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||
:b-label="$t('common.cancel')"
|
||||
:a-label="$t('common.confirm')"
|
||||
:b-label="bLabel"
|
||||
:a-label="aLabel"
|
||||
@activate-b="handleActivateB"
|
||||
@activate-a="handleActivateA"
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,9 @@ const CELL_ANIMATION_STAGGER = 0.02;
|
||||
|
||||
const SNAIL_ORDER = [0, 1, 2, 3, 7, 11, 15, 14, 13, 12, 8, 4, 5, 6, 10, 9];
|
||||
|
||||
const bLabel = ref($t("common.goBack"));
|
||||
const aLabel = ref($t("common.select"));
|
||||
|
||||
// animation state
|
||||
const animation = reactive({
|
||||
cellVisibility: Array(16).fill(0) as number[],
|
||||
@@ -74,6 +77,15 @@ const animateIntro = (): gsap.core.Timeline => {
|
||||
);
|
||||
}
|
||||
|
||||
timeline.call(
|
||||
() => {
|
||||
bLabel.value = $t("common.cancel");
|
||||
aLabel.value = $t("common.confirm");
|
||||
},
|
||||
[],
|
||||
SUBMENU_LABEL_CHANGE_DELAY,
|
||||
);
|
||||
|
||||
return timeline;
|
||||
};
|
||||
|
||||
@@ -287,8 +299,8 @@ const handleActivateA = () => {
|
||||
<template>
|
||||
<CommonButtons
|
||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||
:b-label="$t('common.cancel')"
|
||||
:a-label="$t('common.confirm')"
|
||||
:b-label="bLabel"
|
||||
:a-label="aLabel"
|
||||
@activate-b="handleActivateB"
|
||||
@activate-a="handleActivateA"
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,16 @@ const SCORE_OFFSET = -20;
|
||||
const SCORE_DURATION = 0.167;
|
||||
|
||||
const isAnimating = ref(true);
|
||||
const state = ref<"waiting" | "alive" | "pause" | "dead">("waiting");
|
||||
|
||||
const labelsReady = ref(false);
|
||||
const bLabel = computed(() =>
|
||||
labelsReady.value ? $t("common.quit") : $t("common.goBack"),
|
||||
);
|
||||
const aLabel = computed(() => {
|
||||
if (!labelsReady.value) return $t("common.select");
|
||||
return state.value === "waiting" ? $t("common.start") : $t("common.restart");
|
||||
});
|
||||
|
||||
const intro = reactive({
|
||||
boardOffsetY: BOARD_SLIDE_OFFSET,
|
||||
@@ -31,7 +41,11 @@ const intro = reactive({
|
||||
const animateIntro = async () => {
|
||||
isAnimating.value = true;
|
||||
await gsap
|
||||
.timeline()
|
||||
.timeline({
|
||||
onComplete: () => {
|
||||
isAnimating.value = false;
|
||||
},
|
||||
})
|
||||
.to(
|
||||
intro,
|
||||
{ boardOffsetY: 0, duration: BOARD_SLIDE_DURATION, ease: "none" },
|
||||
@@ -51,14 +65,24 @@ const animateIntro = async () => {
|
||||
intro,
|
||||
{ scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
|
||||
BOARD_SLIDE_DURATION + TEXT_FADE_DURATION,
|
||||
)
|
||||
.call(
|
||||
() => {
|
||||
labelsReady.value = true;
|
||||
},
|
||||
[],
|
||||
SUBMENU_LABEL_CHANGE_DELAY,
|
||||
);
|
||||
isAnimating.value = false;
|
||||
};
|
||||
|
||||
const animateOutro = async () => {
|
||||
isAnimating.value = true;
|
||||
await gsap
|
||||
.timeline()
|
||||
.timeline({
|
||||
onComplete: () => {
|
||||
isAnimating.value = false;
|
||||
},
|
||||
})
|
||||
.to(
|
||||
intro,
|
||||
{
|
||||
@@ -160,7 +184,6 @@ const tail: THREE.Vector2[] = [];
|
||||
const food = new THREE.Vector2();
|
||||
|
||||
let score = 0;
|
||||
const state = ref<"waiting" | "alive" | "pause" | "dead">("waiting");
|
||||
|
||||
const randomFoodPos = (): THREE.Vector2 => {
|
||||
// can't spawn on the head or tail
|
||||
@@ -337,8 +360,8 @@ defineOptions({ render: () => null });
|
||||
<template>
|
||||
<CommonButtons
|
||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||
:b-label="$t('common.quit')"
|
||||
:a-label="state === 'waiting' ? $t('common.start') : $t('common.restart')"
|
||||
:b-label="bLabel"
|
||||
:a-label="aLabel"
|
||||
@activate-b="handleActivateB"
|
||||
@activate-a="handleActivateA"
|
||||
/>
|
||||
|
||||
@@ -19,6 +19,9 @@ const ROW_COUNT = 2;
|
||||
|
||||
const isAnimating = ref(true);
|
||||
|
||||
const bLabel = ref($t("common.goBack"));
|
||||
const aLabel = ref($t("common.select"));
|
||||
|
||||
const animation = reactive({
|
||||
rowOffsetY: new Array<number>(ROW_COUNT).fill(SLIDE_OFFSET),
|
||||
rowOpacity: new Array<number>(ROW_COUNT).fill(0),
|
||||
@@ -26,7 +29,11 @@ const animation = reactive({
|
||||
|
||||
const animateIntro = async () => {
|
||||
isAnimating.value = true;
|
||||
const timeline = gsap.timeline();
|
||||
const timeline = gsap.timeline({
|
||||
onComplete: () => {
|
||||
isAnimating.value = false;
|
||||
},
|
||||
});
|
||||
for (let i = 0; i < ROW_COUNT; i++) {
|
||||
timeline
|
||||
.to(
|
||||
@@ -40,13 +47,24 @@ const animateIntro = async () => {
|
||||
i * ROW_STAGGER,
|
||||
);
|
||||
}
|
||||
timeline.call(
|
||||
() => {
|
||||
bLabel.value = $t("common.cancel");
|
||||
aLabel.value = $t("common.confirm");
|
||||
},
|
||||
[],
|
||||
SUBMENU_LABEL_CHANGE_DELAY,
|
||||
);
|
||||
await timeline;
|
||||
isAnimating.value = false;
|
||||
};
|
||||
|
||||
const animateOutro = async () => {
|
||||
isAnimating.value = true;
|
||||
const timeline = gsap.timeline();
|
||||
const timeline = gsap.timeline({
|
||||
onComplete: () => {
|
||||
isAnimating.value = false;
|
||||
},
|
||||
});
|
||||
for (let i = 0; i < ROW_COUNT; i++) {
|
||||
timeline
|
||||
.to(
|
||||
@@ -128,8 +146,8 @@ defineOptions({ render: () => null });
|
||||
<template>
|
||||
<CommonButtons
|
||||
:y-offset="store.submenuButtonsOffsetY"
|
||||
:b-label="$t('common.cancel')"
|
||||
:a-label="$t('common.confirm')"
|
||||
:b-label="bLabel"
|
||||
:a-label="aLabel"
|
||||
@activate-b="handleActivateB"
|
||||
@activate-a="handleActivateA"
|
||||
/>
|
||||
|
||||
@@ -268,9 +268,16 @@ export const useButtonNavigation = <T extends Record<string, Rect>>({
|
||||
if (nextButton.value) {
|
||||
targetButton = nextButton.value;
|
||||
} else {
|
||||
const leftConfig = getNavigationTarget(currentNav.left);
|
||||
const rightConfig = getNavigationTarget(currentNav.right);
|
||||
targetButton = leftConfig?.target ?? rightConfig?.target;
|
||||
for (const [button, nav] of Object.entries(navigation) as [
|
||||
Entry,
|
||||
(typeof navigation)[Entry],
|
||||
][]) {
|
||||
const downConfig = getNavigationTarget(nav.down);
|
||||
if (downConfig?.target === currentButton) {
|
||||
targetButton = button;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const targetNav = navigation[upConfig.target];
|
||||
@@ -292,9 +299,16 @@ export const useButtonNavigation = <T extends Record<string, Rect>>({
|
||||
if (nextButton.value) {
|
||||
targetButton = nextButton.value;
|
||||
} else {
|
||||
const leftConfig = getNavigationTarget(currentNav.left);
|
||||
const rightConfig = getNavigationTarget(currentNav.right);
|
||||
targetButton = leftConfig?.target ?? rightConfig?.target;
|
||||
for (const [button, nav] of Object.entries(navigation) as [
|
||||
Entry,
|
||||
(typeof navigation)[Entry],
|
||||
][]) {
|
||||
const upConfig = getNavigationTarget(nav.up);
|
||||
if (upConfig?.target === currentButton) {
|
||||
targetButton = button;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const targetNav = navigation[downConfig.target];
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import gsap from "gsap";
|
||||
|
||||
// not sure how i came up with it last night
|
||||
export const SUBMENU_LABEL_CHANGE_DELAY = 0.25 + 0.15 - (0.167 + 0.08);
|
||||
|
||||
export const SETTINGS_MENUS = [
|
||||
"options",
|
||||
"clock",
|
||||
@@ -80,23 +83,9 @@ export const useSettingsStore = defineStore("settings", {
|
||||
{ offsetY: 0, opacity: 1, duration: 0.25, ease: "none" },
|
||||
"+=0.05",
|
||||
)
|
||||
.to(this, {
|
||||
submenuButtonsOffsetY: 24,
|
||||
duration: 0.167,
|
||||
ease: "none",
|
||||
})
|
||||
.call(() => {
|
||||
this.currentSubMenu = submenu;
|
||||
})
|
||||
.to(
|
||||
this,
|
||||
{
|
||||
submenuButtonsOffsetY: 0,
|
||||
duration: 0.167,
|
||||
ease: "none",
|
||||
},
|
||||
"+=0.05",
|
||||
);
|
||||
});
|
||||
|
||||
const achievements = useAchievementsStore();
|
||||
if (!achievements.advancement.visitedSettings.includes(submenu)) {
|
||||
|
||||
18
docker-compose.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
app:
|
||||
image: git.pihkaal.me/pihkaal/pihkaal-me:latest
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- web
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.pihkaal-me.rule=Host(`beta.pihkaal.me`)"
|
||||
- "traefik.http.routers.pihkaal-me.middlewares=auth"
|
||||
- "traefik.http.routers.pihkaal-me.tls=true"
|
||||
- "traefik.http.routers.pihkaal-me.tls.certResolver=myresolver"
|
||||
- "traefik.http.services.pihkaal-me.loadbalancer.server.port=3000"
|
||||
- "traefik.http.middlewares.auth.basicauth.users=${BASIC_AUTH_USERS}"
|
||||
|
||||
networks:
|
||||
web:
|
||||
external: true
|
||||
BIN
public/gallery/P1010002.JPG
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/gallery/P1010006.JPG
Normal file
|
After Width: | Height: | Size: 5.9 MiB |
BIN
public/gallery/P1010011.JPG
Normal file
|
After Width: | Height: | Size: 7.3 MiB |
BIN
public/gallery/P1010012.JPG
Normal file
|
After Width: | Height: | Size: 6.4 MiB |
BIN
public/gallery/P1010016.JPG
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/gallery/P1010017.JPG
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
public/gallery/P1010031.JPG
Normal file
|
After Width: | Height: | Size: 6.7 MiB |
BIN
public/gallery/P1010037.JPG
Executable file
|
After Width: | Height: | Size: 7.4 MiB |
BIN
public/gallery/P1010039.JPG
Normal file
|
After Width: | Height: | Size: 6.4 MiB |
BIN
public/gallery/P1010045.JPG
Normal file
|
After Width: | Height: | Size: 7.3 MiB |
BIN
public/gallery/P1010047.JPG
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
BIN
public/gallery/P1010065.JPG
Normal file
|
After Width: | Height: | Size: 7.1 MiB |
BIN
public/gallery/P1010076.JPG
Normal file
|
After Width: | Height: | Size: 5.9 MiB |
BIN
public/gallery/P1010116.JPG
Normal file
|
After Width: | Height: | Size: 6.0 MiB |
BIN
public/gallery/P1010156.JPG
Normal file
|
After Width: | Height: | Size: 6.4 MiB |
BIN
public/gallery/P1010158.JPG
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
BIN
public/gallery/P1010195.JPG
Normal file
|
After Width: | Height: | Size: 6.6 MiB |
BIN
public/gallery/P1010224.JPG
Normal file
|
After Width: | Height: | Size: 6.2 MiB |
BIN
public/gallery/P1010248.JPG
Normal file
|
After Width: | Height: | Size: 6.6 MiB |
BIN
public/gallery/P1010268.JPG
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/gallery/P1010301.JPG
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
BIN
public/gallery/P1010328.JPG
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/gallery/P1010336.JPG
Normal file
|
After Width: | Height: | Size: 5.9 MiB |
BIN
public/gallery/P1010465.JPG
Normal file
|
After Width: | Height: | Size: 6.4 MiB |
BIN
public/gallery/P1010667.JPG
Normal file
|
After Width: | Height: | Size: 5.7 MiB |
BIN
public/gallery/P1010668.JPG
Normal file
|
After Width: | Height: | Size: 4.9 MiB |
BIN
public/gallery/P1010670.JPG
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
BIN
public/gallery/P1010682.JPG
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
public/gallery/P1010696.JPG
Normal file
|
After Width: | Height: | Size: 5.7 MiB |
BIN
public/gallery/P1010714.JPG
Normal file
|
After Width: | Height: | Size: 6.0 MiB |
BIN
public/gallery/P1010724.JPG
Normal file
|
After Width: | Height: | Size: 5.9 MiB |
BIN
public/gallery/P1010725.JPG
Normal file
|
After Width: | Height: | Size: 5.7 MiB |
BIN
public/gallery/P1010745.JPG
Normal file
|
After Width: | Height: | Size: 5.6 MiB |
BIN
public/gallery/P1010753.JPG
Normal file
|
After Width: | Height: | Size: 6.0 MiB |
BIN
public/gallery/P8120190.JPG
Executable file
|
After Width: | Height: | Size: 6.3 MiB |
BIN
public/gallery/P9100259.JPG
Executable file
|
After Width: | Height: | Size: 6.1 MiB |
BIN
public/gallery/P9100305.JPG
Executable file
|
After Width: | Height: | Size: 7.3 MiB |
BIN
public/gallery/P9100336.JPG
Executable file
|
After Width: | Height: | Size: 5.9 MiB |
BIN
public/gallery/P9100341.JPG
Executable file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
public/gallery/P9100349.JPG
Executable file
|
After Width: | Height: | Size: 7.5 MiB |
BIN
public/gallery/P9100351.JPG
Executable file
|
After Width: | Height: | Size: 7.5 MiB |
BIN
public/gallery/P9100356.JPG
Executable file
|
After Width: | Height: | Size: 6.7 MiB |
BIN
public/gallery/P9100357.JPG
Executable file
|
After Width: | Height: | Size: 7.4 MiB |
BIN
public/gallery/P9100377.JPG
Executable file
|
After Width: | Height: | Size: 7.5 MiB |
BIN
public/gallery/PA170122.JPG
Normal file
|
After Width: | Height: | Size: 7.3 MiB |