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
|
# beta.pihkaal.me
|
||||||
|
|
||||||
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.
|
|
||||||
|
|||||||
@@ -102,21 +102,26 @@ onRender((ctx) => {
|
|||||||
);
|
);
|
||||||
}, 60);
|
}, 60);
|
||||||
|
|
||||||
|
let lastPressedButton: "A" | "B" | null = null;
|
||||||
|
|
||||||
onClick((x, y) => {
|
onClick((x, y) => {
|
||||||
if (props.yOffset !== 0) return;
|
if (props.yOffset !== 0) return;
|
||||||
if (rectContains(B_BUTTON, [x, y])) {
|
if (rectContains(B_BUTTON, [x, y]) && lastPressedButton === "B") {
|
||||||
emit("activateB");
|
emit("activateB");
|
||||||
} else if (rectContains(A_BUTTON, [x, y])) {
|
} else if (rectContains(A_BUTTON, [x, y]) && lastPressedButton === "A") {
|
||||||
emit("activateA");
|
emit("activateA");
|
||||||
}
|
}
|
||||||
|
lastPressedButton = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
onMouseDown((x, y) => {
|
onMouseDown((x, y) => {
|
||||||
if (props.yOffset !== 0) return;
|
if (props.yOffset !== 0) return;
|
||||||
if (rectContains(B_BUTTON, [x, y])) {
|
if (rectContains(B_BUTTON, [x, y])) {
|
||||||
bButtonPressed = true;
|
bButtonPressed = true;
|
||||||
|
lastPressedButton = "B";
|
||||||
} else if (rectContains(A_BUTTON, [x, y])) {
|
} else if (rectContains(A_BUTTON, [x, y])) {
|
||||||
aButtonPressed = true;
|
aButtonPressed = true;
|
||||||
|
lastPressedButton = "A";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,15 @@ const { selected, pressed, selectorPosition } = useButtonNavigation({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
initialButton: "git",
|
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 { action, url } = BUTTONS[button];
|
||||||
const verb = $t(`contact.actions.${button}`);
|
const verb = $t(`contact.actions.${button}`);
|
||||||
if (action === "copy") {
|
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 = () => {
|
const handleActivateB = () => {
|
||||||
if (store.isIntro || store.isOutro) return;
|
if (store.isIntro || store.isOutro) return;
|
||||||
@@ -157,6 +159,7 @@ onRender((ctx) => {
|
|||||||
"
|
"
|
||||||
:b-label="$t('common.quit')"
|
:b-label="$t('common.quit')"
|
||||||
:a-label="$t(`contact.actions.${BUTTONS[selected].action}`)"
|
:a-label="$t(`contact.actions.${BUTTONS[selected].action}`)"
|
||||||
|
@activate-a="handleActivateA(selected)"
|
||||||
@activate-b="handleActivateB"
|
@activate-b="handleActivateB"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -274,51 +274,103 @@ const pressButton = (button: string) => {
|
|||||||
window.dispatchEvent(new KeyboardEvent("keydown", { key: `NDS_${button}` }));
|
window.dispatchEvent(new KeyboardEvent("keydown", { key: `NDS_${button}` }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (event: MouseEvent) => {
|
const raycast = (clientX: number, clientY: number) => {
|
||||||
if (!hasAnimated.value) {
|
|
||||||
animateIntro();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const domElement = renderer.instance.domElement;
|
const domElement = renderer.instance.domElement;
|
||||||
const rect = domElement.getBoundingClientRect();
|
const rect = domElement.getBoundingClientRect();
|
||||||
|
|
||||||
const raycaster = new THREE.Raycaster();
|
const raycaster = new THREE.Raycaster();
|
||||||
raycaster.setFromCamera(
|
raycaster.setFromCamera(
|
||||||
new THREE.Vector2(
|
new THREE.Vector2(
|
||||||
((event.clientX - rect.left) / rect.width) * 2 - 1,
|
((clientX - rect.left) / rect.width) * 2 - 1,
|
||||||
-((event.clientY - rect.top) / rect.height) * 2 + 1,
|
-((clientY - rect.top) / rect.height) * 2 + 1,
|
||||||
),
|
),
|
||||||
camera.activeCamera.value,
|
camera.activeCamera.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
const intersects = raycaster.intersectObjects(model.children, true);
|
const intersects = raycaster.intersectObjects(model.children, true);
|
||||||
const intersection = intersects[0];
|
return intersects[0];
|
||||||
if (!intersection?.uv) return;
|
};
|
||||||
|
|
||||||
switch (intersection.object.name) {
|
const getScreenCanvas = (name: string) => {
|
||||||
case TOP_SCREEN:
|
if (name === TOP_SCREEN) return props.topScreenCanvas;
|
||||||
case BOTTOM_SCREEN: {
|
if (name === BOTTOM_SCREEN) return props.bottomScreenCanvas;
|
||||||
const canvas =
|
return null;
|
||||||
intersection.object.name === TOP_SCREEN
|
};
|
||||||
? props.topScreenCanvas
|
|
||||||
: props.bottomScreenCanvas;
|
|
||||||
|
|
||||||
if (!canvas) break;
|
const toScreenCoords = (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
intersection: THREE.Intersection,
|
||||||
|
) => {
|
||||||
|
if (!intersection.uv) return null;
|
||||||
|
|
||||||
const logicalX = (1 - intersection.uv.x) * LOGICAL_WIDTH;
|
const logicalX = (1 - intersection.uv.x) * LOGICAL_WIDTH;
|
||||||
const logicalY = (1 - intersection.uv.y) * LOGICAL_HEIGHT;
|
const logicalY = (1 - intersection.uv.y) * LOGICAL_HEIGHT;
|
||||||
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
canvas.dispatchEvent(
|
return {
|
||||||
new MouseEvent("click", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
clientX: (logicalX / LOGICAL_WIDTH) * rect.width + rect.left,
|
clientX: (logicalX / LOGICAL_WIDTH) * rect.width + rect.left,
|
||||||
clientY: (logicalY / LOGICAL_HEIGHT) * rect.height + rect.top,
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,37 +391,84 @@ const handleClick = (event: MouseEvent) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case X_BUTTON:
|
default: {
|
||||||
case A_BUTTON:
|
const button =
|
||||||
case Y_BUTTON:
|
BUTTON_MAP[intersection.object.name as keyof typeof BUTTON_MAP];
|
||||||
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];
|
|
||||||
if (button) pressButton(button);
|
if (button) pressButton(button);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const releaseButton = () => {
|
||||||
if (mousePressedButton) {
|
if (!mousePressedButton) return;
|
||||||
physicalButtonsDown.delete(mousePressedButton);
|
|
||||||
|
|
||||||
|
physicalButtonsDown.delete(mousePressedButton);
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new KeyboardEvent("keyup", { key: `NDS_${mousePressedButton}` }),
|
new KeyboardEvent("keyup", { key: `NDS_${mousePressedButton}` }),
|
||||||
);
|
);
|
||||||
|
|
||||||
mousePressedButton = null;
|
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;
|
app.ready = true;
|
||||||
|
|
||||||
if (renderer) {
|
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("mouseup", handleMouseUp);
|
||||||
|
renderer.instance.domElement.addEventListener(
|
||||||
|
"touchstart",
|
||||||
|
handleTouchStart,
|
||||||
|
);
|
||||||
|
renderer.instance.domElement.addEventListener("touchend", handleTouchEnd);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (renderer) {
|
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("mouseup", handleMouseUp);
|
||||||
|
renderer.instance.domElement.removeEventListener(
|
||||||
|
"touchstart",
|
||||||
|
handleTouchStart,
|
||||||
|
);
|
||||||
|
renderer.instance.domElement.removeEventListener(
|
||||||
|
"touchend",
|
||||||
|
handleTouchEnd,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
topScreenTexture?.dispose();
|
topScreenTexture?.dispose();
|
||||||
bottomScreenTexture?.dispose();
|
bottomScreenTexture?.dispose();
|
||||||
|
|||||||
@@ -96,14 +96,29 @@ const dispatchSwipe = (endX: number, endY: number) => {
|
|||||||
const handleTouchStart = (event: TouchEvent) => {
|
const handleTouchStart = (event: TouchEvent) => {
|
||||||
const touch = event.touches[0];
|
const touch = event.touches[0];
|
||||||
if (!touch) return;
|
if (!touch) return;
|
||||||
|
|
||||||
swipeStartX = touch.clientX;
|
swipeStartX = touch.clientX;
|
||||||
swipeStartY = touch.clientY;
|
swipeStartY = touch.clientY;
|
||||||
|
|
||||||
|
canvas.value?.dispatchEvent(
|
||||||
|
new MouseEvent("mousedown", {
|
||||||
|
clientX: touch.clientX,
|
||||||
|
clientY: touch.clientY,
|
||||||
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTouchEnd = (event: TouchEvent) => {
|
const handleTouchEnd = (event: TouchEvent) => {
|
||||||
const touch = event.changedTouches[0];
|
const touch = event.changedTouches[0];
|
||||||
if (!touch) return;
|
if (!touch) return;
|
||||||
|
|
||||||
dispatchSwipe(touch.clientX, touch.clientY);
|
dispatchSwipe(touch.clientX, touch.clientY);
|
||||||
|
document.dispatchEvent(
|
||||||
|
new MouseEvent("mouseup", {
|
||||||
|
clientX: touch.clientX,
|
||||||
|
clientY: touch.clientY,
|
||||||
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
let mouseSwiping = false;
|
let mouseSwiping = false;
|
||||||
@@ -178,7 +193,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
canvas.value.addEventListener("touchend", handleTouchEnd, { passive: true });
|
canvas.value.addEventListener("touchend", handleTouchEnd, { passive: true });
|
||||||
canvas.value.addEventListener("mousedown", handleSwipeMouseDown);
|
canvas.value.addEventListener("mousedown", handleSwipeMouseDown);
|
||||||
document.addEventListener("mouseup", handleSwipeMouseUp);
|
canvas.value.addEventListener("mouseup", handleSwipeMouseUp);
|
||||||
|
|
||||||
animationFrameId = requestAnimationFrame(renderFrame);
|
animationFrameId = requestAnimationFrame(renderFrame);
|
||||||
});
|
});
|
||||||
@@ -195,8 +210,8 @@ onUnmounted(() => {
|
|||||||
canvas.value.removeEventListener("touchstart", handleTouchStart);
|
canvas.value.removeEventListener("touchstart", handleTouchStart);
|
||||||
canvas.value.removeEventListener("touchend", handleTouchEnd);
|
canvas.value.removeEventListener("touchend", handleTouchEnd);
|
||||||
canvas.value.removeEventListener("mousedown", handleSwipeMouseDown);
|
canvas.value.removeEventListener("mousedown", handleSwipeMouseDown);
|
||||||
|
canvas.value.removeEventListener("mouseup", handleSwipeMouseUp);
|
||||||
}
|
}
|
||||||
document.removeEventListener("mouseup", handleSwipeMouseUp);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ const confirmationModal = useConfirmationModal();
|
|||||||
|
|
||||||
const isAnimating = ref(true);
|
const isAnimating = ref(true);
|
||||||
|
|
||||||
|
const bLabel = ref($t("common.goBack"));
|
||||||
|
const aLabel = ref($t("common.select"));
|
||||||
|
|
||||||
const SLIDE_OFFSET = 96;
|
const SLIDE_OFFSET = 96;
|
||||||
const SLIDE_DURATION = 0.25;
|
const SLIDE_DURATION = 0.25;
|
||||||
const ARROW_SLIDE_DELAY = 0.15;
|
const ARROW_SLIDE_DELAY = 0.15;
|
||||||
@@ -60,6 +63,14 @@ const animateIntro = async () => {
|
|||||||
.timeline()
|
.timeline()
|
||||||
.to(animation, { offsetY: 0, duration: SLIDE_DURATION, ease: "none" }, 0)
|
.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.reset");
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
SUBMENU_LABEL_CHANGE_DELAY,
|
||||||
|
)
|
||||||
.to(
|
.to(
|
||||||
animation,
|
animation,
|
||||||
{ viewAllOffsetY: 0, duration: ARROW_SLIDE_DURATION, ease: "none" },
|
{ viewAllOffsetY: 0, duration: ARROW_SLIDE_DURATION, ease: "none" },
|
||||||
@@ -197,8 +208,8 @@ onRender((ctx) => {
|
|||||||
|
|
||||||
<CommonButtons
|
<CommonButtons
|
||||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||||
:b-label="$t('common.cancel')"
|
:b-label="bLabel"
|
||||||
:a-label="$t('common.reset')"
|
:a-label="aLabel"
|
||||||
@activate-b="handleActivateB()"
|
@activate-b="handleActivateB()"
|
||||||
@activate-a="handleReset()"
|
@activate-a="handleReset()"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SettingsBottomScreenNumberInput as NumberInput } from "#components";
|
import { SettingsBottomScreenNumberInput as NumberInput } from "#components";
|
||||||
import { useIntervalFn } from "@vueuse/core";
|
import { useIntervalFn } from "@vueuse/core";
|
||||||
|
import gsap from "gsap";
|
||||||
|
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
|
|
||||||
@@ -12,6 +13,9 @@ useIntervalFn(() => {
|
|||||||
|
|
||||||
const isAnimating = ref(true);
|
const isAnimating = ref(true);
|
||||||
|
|
||||||
|
const bLabel = ref($t("common.goBack"));
|
||||||
|
const aLabel = ref($t("common.select"));
|
||||||
|
|
||||||
const monthRef = useTemplateRef<InstanceType<typeof NumberInput>>("month");
|
const monthRef = useTemplateRef<InstanceType<typeof NumberInput>>("month");
|
||||||
const dayRef = useTemplateRef<InstanceType<typeof NumberInput>>("day");
|
const dayRef = useTemplateRef<InstanceType<typeof NumberInput>>("day");
|
||||||
const yearRef = useTemplateRef<InstanceType<typeof NumberInput>>("year");
|
const yearRef = useTemplateRef<InstanceType<typeof NumberInput>>("year");
|
||||||
@@ -22,6 +26,14 @@ const animateIntro = async () => {
|
|||||||
monthRef.value?.animateIntro(),
|
monthRef.value?.animateIntro(),
|
||||||
dayRef.value?.animateIntro(),
|
dayRef.value?.animateIntro(),
|
||||||
yearRef.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;
|
isAnimating.value = false;
|
||||||
};
|
};
|
||||||
@@ -80,8 +92,8 @@ defineOptions({ render: () => null });
|
|||||||
|
|
||||||
<CommonButtons
|
<CommonButtons
|
||||||
:y-offset="store.submenuButtonsOffsetY"
|
:y-offset="store.submenuButtonsOffsetY"
|
||||||
:b-label="$t('common.cancel')"
|
:b-label="bLabel"
|
||||||
:a-label="$t('common.confirm')"
|
:a-label="aLabel"
|
||||||
@activate-b="handleActivateB"
|
@activate-b="handleActivateB"
|
||||||
@activate-a="handleActivateA"
|
@activate-a="handleActivateA"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ const animation = reactive({
|
|||||||
|
|
||||||
const isAnimating = ref(true);
|
const isAnimating = ref(true);
|
||||||
|
|
||||||
|
const bLabel = ref($t("common.goBack"));
|
||||||
|
const aLabel = ref($t("common.select"));
|
||||||
|
|
||||||
const hourRef = useTemplateRef<InstanceType<typeof NumberInput>>("hour");
|
const hourRef = useTemplateRef<InstanceType<typeof NumberInput>>("hour");
|
||||||
const minuteRef = useTemplateRef<InstanceType<typeof NumberInput>>("minute");
|
const minuteRef = useTemplateRef<InstanceType<typeof NumberInput>>("minute");
|
||||||
|
|
||||||
@@ -34,7 +37,15 @@ const animateIntro = async () => {
|
|||||||
gsap
|
gsap
|
||||||
.timeline()
|
.timeline()
|
||||||
.to(animation, { offsetY: 0, duration: SLIDE_DURATION, ease: "none" }, 0)
|
.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;
|
isAnimating.value = false;
|
||||||
};
|
};
|
||||||
@@ -98,8 +109,8 @@ defineOptions({ render: () => null });
|
|||||||
|
|
||||||
<CommonButtons
|
<CommonButtons
|
||||||
:y-offset="store.submenuButtonsOffsetY"
|
:y-offset="store.submenuButtonsOffsetY"
|
||||||
:b-label="$t('common.cancel')"
|
:b-label="bLabel"
|
||||||
:a-label="$t('common.confirm')"
|
:a-label="aLabel"
|
||||||
@activate-b="handleActivateB"
|
@activate-b="handleActivateB"
|
||||||
@activate-a="handleActivateA"
|
@activate-a="handleActivateA"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -250,6 +250,21 @@ const selectorTransitionOffsetY = computed(() => {
|
|||||||
return store.submenuTransition.offsetY;
|
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 = () => {
|
const handleActivateB = () => {
|
||||||
if (store.isIntro || store.isOutro || store.submenuTransition.opacity < 1)
|
if (store.isIntro || store.isOutro || store.submenuTransition.opacity < 1)
|
||||||
return;
|
return;
|
||||||
@@ -318,6 +333,7 @@ const handleActivateB = () => {
|
|||||||
:y-offset="store.barOffsetY + store.submenuButtonsOffsetY"
|
:y-offset="store.barOffsetY + store.submenuButtonsOffsetY"
|
||||||
:b-label="isSubmenuSelected ? $t('common.goBack') : $t('common.quit')"
|
:b-label="isSubmenuSelected ? $t('common.goBack') : $t('common.quit')"
|
||||||
:a-label="$t('common.select')"
|
:a-label="$t('common.select')"
|
||||||
|
@activate-a="handleActivateA()"
|
||||||
@activate-b="handleActivateB()"
|
@activate-b="handleActivateB()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ const SCORE_DURATION = 0.167;
|
|||||||
|
|
||||||
const isAnimating = ref(true);
|
const isAnimating = ref(true);
|
||||||
|
|
||||||
|
const bLabel = ref($t("common.goBack"));
|
||||||
|
const aLabel = ref($t("common.select"));
|
||||||
|
|
||||||
const intro = reactive({
|
const intro = reactive({
|
||||||
frameOffsetY: SLIDE_OFFSET,
|
frameOffsetY: SLIDE_OFFSET,
|
||||||
frameOpacity: 0,
|
frameOpacity: 0,
|
||||||
@@ -148,7 +151,11 @@ const animateIntro = async () => {
|
|||||||
buildTilesFromBoard();
|
buildTilesFromBoard();
|
||||||
|
|
||||||
await gsap
|
await gsap
|
||||||
.timeline()
|
.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
isAnimating.value = false;
|
||||||
|
},
|
||||||
|
})
|
||||||
.to(intro, { frameOffsetY: 0, duration: SLIDE_DURATION, ease: "none" }, 0)
|
.to(intro, { frameOffsetY: 0, duration: SLIDE_DURATION, ease: "none" }, 0)
|
||||||
.to(intro, { frameOpacity: 1, duration: SLIDE_DURATION, ease: "none" }, 0)
|
.to(intro, { frameOpacity: 1, duration: SLIDE_DURATION, ease: "none" }, 0)
|
||||||
.call(
|
.call(
|
||||||
@@ -163,14 +170,25 @@ const animateIntro = async () => {
|
|||||||
intro,
|
intro,
|
||||||
{ scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
|
{ scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
|
||||||
SLIDE_DURATION,
|
SLIDE_DURATION,
|
||||||
|
)
|
||||||
|
.call(
|
||||||
|
() => {
|
||||||
|
bLabel.value = $t("common.quit");
|
||||||
|
aLabel.value = $t("common.restart");
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
SUBMENU_LABEL_CHANGE_DELAY,
|
||||||
);
|
);
|
||||||
isAnimating.value = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const animateOutro = async () => {
|
const animateOutro = async () => {
|
||||||
isAnimating.value = true;
|
isAnimating.value = true;
|
||||||
await gsap
|
await gsap
|
||||||
.timeline()
|
.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
isAnimating.value = false;
|
||||||
|
},
|
||||||
|
})
|
||||||
.to(
|
.to(
|
||||||
intro,
|
intro,
|
||||||
{ frameOffsetY: SLIDE_OFFSET, duration: SLIDE_DURATION, ease: "none" },
|
{ frameOffsetY: SLIDE_OFFSET, duration: SLIDE_DURATION, ease: "none" },
|
||||||
@@ -581,8 +599,8 @@ useKeyDown(({ key }) => {
|
|||||||
<template>
|
<template>
|
||||||
<CommonButtons
|
<CommonButtons
|
||||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||||
:b-label="$t('common.quit')"
|
:b-label="bLabel"
|
||||||
:a-label="$t('common.restart')"
|
:a-label="aLabel"
|
||||||
@activate-b="handleActivateB()"
|
@activate-b="handleActivateB()"
|
||||||
@activate-a="handleActivateA()"
|
@activate-a="handleActivateA()"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ const BUTTON_POSITIONS = [
|
|||||||
|
|
||||||
const isAnimating = ref(true);
|
const isAnimating = ref(true);
|
||||||
|
|
||||||
|
const bLabel = ref($t("common.goBack"));
|
||||||
|
const aLabel = ref($t("common.select"));
|
||||||
|
|
||||||
const { selected, selectorPosition } = useButtonNavigation({
|
const { selected, selectorPosition } = useButtonNavigation({
|
||||||
buttons: {
|
buttons: {
|
||||||
english: [10, 27, 106, 41],
|
english: [10, 27, 106, 41],
|
||||||
@@ -95,7 +98,11 @@ const animation = reactive({
|
|||||||
|
|
||||||
const animateIntro = async () => {
|
const animateIntro = async () => {
|
||||||
isAnimating.value = true;
|
isAnimating.value = true;
|
||||||
const timeline = gsap.timeline();
|
const timeline = gsap.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
isAnimating.value = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
for (let i = 0; i < ROW_COUNT; i++) {
|
for (let i = 0; i < ROW_COUNT; i++) {
|
||||||
timeline
|
timeline
|
||||||
.to(
|
.to(
|
||||||
@@ -109,13 +116,24 @@ const animateIntro = async () => {
|
|||||||
i * ROW_STAGGER,
|
i * ROW_STAGGER,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
timeline.call(
|
||||||
|
() => {
|
||||||
|
bLabel.value = $t("common.cancel");
|
||||||
|
aLabel.value = $t("common.confirm");
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
SUBMENU_LABEL_CHANGE_DELAY,
|
||||||
|
);
|
||||||
await timeline;
|
await timeline;
|
||||||
isAnimating.value = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const animateOutro = async () => {
|
const animateOutro = async () => {
|
||||||
isAnimating.value = true;
|
isAnimating.value = true;
|
||||||
const timeline = gsap.timeline();
|
const timeline = gsap.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
isAnimating.value = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
for (let i = 0; i < ROW_COUNT; i++) {
|
for (let i = 0; i < ROW_COUNT; i++) {
|
||||||
timeline
|
timeline
|
||||||
.to(
|
.to(
|
||||||
@@ -205,8 +223,9 @@ defineOptions({
|
|||||||
<template>
|
<template>
|
||||||
<CommonButtons
|
<CommonButtons
|
||||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||||
:b-label="$t('common.cancel')"
|
:b-label="bLabel"
|
||||||
:a-label="$t('common.confirm')"
|
:a-label="aLabel"
|
||||||
|
@activate-a="handleActivateA"
|
||||||
@activate-b="handleActivateB"
|
@activate-b="handleActivateB"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ const SLIDE_DURATION = 0.25;
|
|||||||
|
|
||||||
const isAnimating = ref(true);
|
const isAnimating = ref(true);
|
||||||
|
|
||||||
|
const bLabel = ref($t("common.goBack"));
|
||||||
|
const aLabel = ref($t("common.select"));
|
||||||
|
|
||||||
const animation = reactive({
|
const animation = reactive({
|
||||||
_3dMode: { headerOffsetY: HEADER_HEIGHT * 3, opacity: 0 },
|
_3dMode: { headerOffsetY: HEADER_HEIGHT * 3, opacity: 0 },
|
||||||
_2dMode: { headerOffsetY: HEADER_HEIGHT * 3, opacity: 0 },
|
_2dMode: { headerOffsetY: HEADER_HEIGHT * 3, opacity: 0 },
|
||||||
@@ -48,6 +51,14 @@ const animateIntro = async () => {
|
|||||||
ease: "none",
|
ease: "none",
|
||||||
},
|
},
|
||||||
BUTTON_STAGGER,
|
BUTTON_STAGGER,
|
||||||
|
)
|
||||||
|
.call(
|
||||||
|
() => {
|
||||||
|
bLabel.value = $t("common.cancel");
|
||||||
|
aLabel.value = $t("common.confirm");
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
SUBMENU_LABEL_CHANGE_DELAY,
|
||||||
);
|
);
|
||||||
isAnimating.value = false;
|
isAnimating.value = false;
|
||||||
};
|
};
|
||||||
@@ -183,8 +194,9 @@ onRender((ctx) => {
|
|||||||
<template>
|
<template>
|
||||||
<CommonButtons
|
<CommonButtons
|
||||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||||
:b-label="$t('common.cancel')"
|
:b-label="bLabel"
|
||||||
:a-label="$t('common.confirm')"
|
:a-label="aLabel"
|
||||||
|
@activate-a="handleActivateA"
|
||||||
@activate-b="handleActivateB"
|
@activate-b="handleActivateB"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,17 @@ let isNewBest = false;
|
|||||||
|
|
||||||
const isAnimating = ref(true);
|
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 AREA_FADE_DURATION = 0.2;
|
||||||
const SCORE_OFFSET = -20;
|
const SCORE_OFFSET = -20;
|
||||||
const SCORE_DURATION = 0.167;
|
const SCORE_DURATION = 0.167;
|
||||||
@@ -81,7 +92,11 @@ const animation = reactive({
|
|||||||
const animateIntro = async () => {
|
const animateIntro = async () => {
|
||||||
isAnimating.value = true;
|
isAnimating.value = true;
|
||||||
await gsap
|
await gsap
|
||||||
.timeline()
|
.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
isAnimating.value = false;
|
||||||
|
},
|
||||||
|
})
|
||||||
.to(
|
.to(
|
||||||
animation,
|
animation,
|
||||||
{ areaOpacity: 1, duration: AREA_FADE_DURATION, ease: "none" },
|
{ areaOpacity: 1, duration: AREA_FADE_DURATION, ease: "none" },
|
||||||
@@ -91,8 +106,15 @@ const animateIntro = async () => {
|
|||||||
animation,
|
animation,
|
||||||
{ scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
|
{ scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
|
||||||
AREA_FADE_DURATION,
|
AREA_FADE_DURATION,
|
||||||
|
)
|
||||||
|
.call(
|
||||||
|
() => {
|
||||||
|
labelsReady.value = true;
|
||||||
|
buttonsRef.value?.forceAnimateBLabel();
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
SUBMENU_LABEL_CHANGE_DELAY,
|
||||||
);
|
);
|
||||||
isAnimating.value = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const animateOutro = async () => {
|
const animateOutro = async () => {
|
||||||
@@ -101,7 +123,11 @@ const animateOutro = async () => {
|
|||||||
targetY = LOGICAL_HEIGHT * 2 - 20;
|
targetY = LOGICAL_HEIGHT * 2 - 20;
|
||||||
|
|
||||||
await gsap
|
await gsap
|
||||||
.timeline()
|
.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
isAnimating.value = false;
|
||||||
|
},
|
||||||
|
})
|
||||||
.to(
|
.to(
|
||||||
animation,
|
animation,
|
||||||
{ areaOpacity: 0, duration: AREA_FADE_DURATION, ease: "none" },
|
{ areaOpacity: 0, duration: AREA_FADE_DURATION, ease: "none" },
|
||||||
@@ -418,9 +444,10 @@ defineOptions({ render: () => null });
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CommonButtons
|
<CommonButtons
|
||||||
|
ref="buttons"
|
||||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||||
:b-label="$t('common.quit')"
|
:b-label="bLabel"
|
||||||
:a-label="state === 'waiting' ? $t('common.start') : $t('common.restart')"
|
:a-label="aLabel"
|
||||||
@activate-b="handleActivateB"
|
@activate-b="handleActivateB"
|
||||||
@activate-a="handleActivateA"
|
@activate-a="handleActivateA"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SettingsBottomScreenNumberInput as NumberInput } from "#components";
|
import { SettingsBottomScreenNumberInput as NumberInput } from "#components";
|
||||||
|
import gsap from "gsap";
|
||||||
|
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
const confirmationModal = useConfirmationModal();
|
const confirmationModal = useConfirmationModal();
|
||||||
@@ -10,6 +11,9 @@ const BIRTHDAY_YEAR = 2002;
|
|||||||
|
|
||||||
const isAnimating = ref(true);
|
const isAnimating = ref(true);
|
||||||
|
|
||||||
|
const bLabel = ref($t("common.goBack"));
|
||||||
|
const aLabel = ref($t("common.select"));
|
||||||
|
|
||||||
const monthRef = useTemplateRef<InstanceType<typeof NumberInput>>("month");
|
const monthRef = useTemplateRef<InstanceType<typeof NumberInput>>("month");
|
||||||
const dayRef = useTemplateRef<InstanceType<typeof NumberInput>>("day");
|
const dayRef = useTemplateRef<InstanceType<typeof NumberInput>>("day");
|
||||||
const yearRef = useTemplateRef<InstanceType<typeof NumberInput>>("year");
|
const yearRef = useTemplateRef<InstanceType<typeof NumberInput>>("year");
|
||||||
@@ -20,6 +24,14 @@ const animateIntro = async () => {
|
|||||||
monthRef.value?.animateIntro(),
|
monthRef.value?.animateIntro(),
|
||||||
dayRef.value?.animateIntro(),
|
dayRef.value?.animateIntro(),
|
||||||
yearRef.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;
|
isAnimating.value = false;
|
||||||
};
|
};
|
||||||
@@ -113,8 +125,8 @@ defineOptions({ render: () => null });
|
|||||||
|
|
||||||
<CommonButtons
|
<CommonButtons
|
||||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||||
:b-label="$t('common.cancel')"
|
:b-label="bLabel"
|
||||||
:a-label="$t('common.confirm')"
|
:a-label="aLabel"
|
||||||
@activate-b="handleActivateB"
|
@activate-b="handleActivateB"
|
||||||
@activate-a="handleActivateA"
|
@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 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
|
// animation state
|
||||||
const animation = reactive({
|
const animation = reactive({
|
||||||
cellVisibility: Array(16).fill(0) as number[],
|
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;
|
return timeline;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -287,8 +299,8 @@ const handleActivateA = () => {
|
|||||||
<template>
|
<template>
|
||||||
<CommonButtons
|
<CommonButtons
|
||||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||||
:b-label="$t('common.cancel')"
|
:b-label="bLabel"
|
||||||
:a-label="$t('common.confirm')"
|
:a-label="aLabel"
|
||||||
@activate-b="handleActivateB"
|
@activate-b="handleActivateB"
|
||||||
@activate-a="handleActivateA"
|
@activate-a="handleActivateA"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ const SCORE_OFFSET = -20;
|
|||||||
const SCORE_DURATION = 0.167;
|
const SCORE_DURATION = 0.167;
|
||||||
|
|
||||||
const isAnimating = ref(true);
|
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({
|
const intro = reactive({
|
||||||
boardOffsetY: BOARD_SLIDE_OFFSET,
|
boardOffsetY: BOARD_SLIDE_OFFSET,
|
||||||
@@ -31,7 +41,11 @@ const intro = reactive({
|
|||||||
const animateIntro = async () => {
|
const animateIntro = async () => {
|
||||||
isAnimating.value = true;
|
isAnimating.value = true;
|
||||||
await gsap
|
await gsap
|
||||||
.timeline()
|
.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
isAnimating.value = false;
|
||||||
|
},
|
||||||
|
})
|
||||||
.to(
|
.to(
|
||||||
intro,
|
intro,
|
||||||
{ boardOffsetY: 0, duration: BOARD_SLIDE_DURATION, ease: "none" },
|
{ boardOffsetY: 0, duration: BOARD_SLIDE_DURATION, ease: "none" },
|
||||||
@@ -51,14 +65,24 @@ const animateIntro = async () => {
|
|||||||
intro,
|
intro,
|
||||||
{ scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
|
{ scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
|
||||||
BOARD_SLIDE_DURATION + TEXT_FADE_DURATION,
|
BOARD_SLIDE_DURATION + TEXT_FADE_DURATION,
|
||||||
|
)
|
||||||
|
.call(
|
||||||
|
() => {
|
||||||
|
labelsReady.value = true;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
SUBMENU_LABEL_CHANGE_DELAY,
|
||||||
);
|
);
|
||||||
isAnimating.value = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const animateOutro = async () => {
|
const animateOutro = async () => {
|
||||||
isAnimating.value = true;
|
isAnimating.value = true;
|
||||||
await gsap
|
await gsap
|
||||||
.timeline()
|
.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
isAnimating.value = false;
|
||||||
|
},
|
||||||
|
})
|
||||||
.to(
|
.to(
|
||||||
intro,
|
intro,
|
||||||
{
|
{
|
||||||
@@ -160,7 +184,6 @@ const tail: THREE.Vector2[] = [];
|
|||||||
const food = new THREE.Vector2();
|
const food = new THREE.Vector2();
|
||||||
|
|
||||||
let score = 0;
|
let score = 0;
|
||||||
const state = ref<"waiting" | "alive" | "pause" | "dead">("waiting");
|
|
||||||
|
|
||||||
const randomFoodPos = (): THREE.Vector2 => {
|
const randomFoodPos = (): THREE.Vector2 => {
|
||||||
// can't spawn on the head or tail
|
// can't spawn on the head or tail
|
||||||
@@ -337,8 +360,8 @@ defineOptions({ render: () => null });
|
|||||||
<template>
|
<template>
|
||||||
<CommonButtons
|
<CommonButtons
|
||||||
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
:y-offset="confirmationModal.buttonsYOffset + store.submenuButtonsOffsetY"
|
||||||
:b-label="$t('common.quit')"
|
:b-label="bLabel"
|
||||||
:a-label="state === 'waiting' ? $t('common.start') : $t('common.restart')"
|
:a-label="aLabel"
|
||||||
@activate-b="handleActivateB"
|
@activate-b="handleActivateB"
|
||||||
@activate-a="handleActivateA"
|
@activate-a="handleActivateA"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ const ROW_COUNT = 2;
|
|||||||
|
|
||||||
const isAnimating = ref(true);
|
const isAnimating = ref(true);
|
||||||
|
|
||||||
|
const bLabel = ref($t("common.goBack"));
|
||||||
|
const aLabel = ref($t("common.select"));
|
||||||
|
|
||||||
const animation = reactive({
|
const animation = reactive({
|
||||||
rowOffsetY: new Array<number>(ROW_COUNT).fill(SLIDE_OFFSET),
|
rowOffsetY: new Array<number>(ROW_COUNT).fill(SLIDE_OFFSET),
|
||||||
rowOpacity: new Array<number>(ROW_COUNT).fill(0),
|
rowOpacity: new Array<number>(ROW_COUNT).fill(0),
|
||||||
@@ -26,7 +29,11 @@ const animation = reactive({
|
|||||||
|
|
||||||
const animateIntro = async () => {
|
const animateIntro = async () => {
|
||||||
isAnimating.value = true;
|
isAnimating.value = true;
|
||||||
const timeline = gsap.timeline();
|
const timeline = gsap.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
isAnimating.value = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
for (let i = 0; i < ROW_COUNT; i++) {
|
for (let i = 0; i < ROW_COUNT; i++) {
|
||||||
timeline
|
timeline
|
||||||
.to(
|
.to(
|
||||||
@@ -40,13 +47,24 @@ const animateIntro = async () => {
|
|||||||
i * ROW_STAGGER,
|
i * ROW_STAGGER,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
timeline.call(
|
||||||
|
() => {
|
||||||
|
bLabel.value = $t("common.cancel");
|
||||||
|
aLabel.value = $t("common.confirm");
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
SUBMENU_LABEL_CHANGE_DELAY,
|
||||||
|
);
|
||||||
await timeline;
|
await timeline;
|
||||||
isAnimating.value = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const animateOutro = async () => {
|
const animateOutro = async () => {
|
||||||
isAnimating.value = true;
|
isAnimating.value = true;
|
||||||
const timeline = gsap.timeline();
|
const timeline = gsap.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
isAnimating.value = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
for (let i = 0; i < ROW_COUNT; i++) {
|
for (let i = 0; i < ROW_COUNT; i++) {
|
||||||
timeline
|
timeline
|
||||||
.to(
|
.to(
|
||||||
@@ -128,8 +146,8 @@ defineOptions({ render: () => null });
|
|||||||
<template>
|
<template>
|
||||||
<CommonButtons
|
<CommonButtons
|
||||||
:y-offset="store.submenuButtonsOffsetY"
|
:y-offset="store.submenuButtonsOffsetY"
|
||||||
:b-label="$t('common.cancel')"
|
:b-label="bLabel"
|
||||||
:a-label="$t('common.confirm')"
|
:a-label="aLabel"
|
||||||
@activate-b="handleActivateB"
|
@activate-b="handleActivateB"
|
||||||
@activate-a="handleActivateA"
|
@activate-a="handleActivateA"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -268,9 +268,16 @@ export const useButtonNavigation = <T extends Record<string, Rect>>({
|
|||||||
if (nextButton.value) {
|
if (nextButton.value) {
|
||||||
targetButton = nextButton.value;
|
targetButton = nextButton.value;
|
||||||
} else {
|
} else {
|
||||||
const leftConfig = getNavigationTarget(currentNav.left);
|
for (const [button, nav] of Object.entries(navigation) as [
|
||||||
const rightConfig = getNavigationTarget(currentNav.right);
|
Entry,
|
||||||
targetButton = leftConfig?.target ?? rightConfig?.target;
|
(typeof navigation)[Entry],
|
||||||
|
][]) {
|
||||||
|
const downConfig = getNavigationTarget(nav.down);
|
||||||
|
if (downConfig?.target === currentButton) {
|
||||||
|
targetButton = button;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const targetNav = navigation[upConfig.target];
|
const targetNav = navigation[upConfig.target];
|
||||||
@@ -292,9 +299,16 @@ export const useButtonNavigation = <T extends Record<string, Rect>>({
|
|||||||
if (nextButton.value) {
|
if (nextButton.value) {
|
||||||
targetButton = nextButton.value;
|
targetButton = nextButton.value;
|
||||||
} else {
|
} else {
|
||||||
const leftConfig = getNavigationTarget(currentNav.left);
|
for (const [button, nav] of Object.entries(navigation) as [
|
||||||
const rightConfig = getNavigationTarget(currentNav.right);
|
Entry,
|
||||||
targetButton = leftConfig?.target ?? rightConfig?.target;
|
(typeof navigation)[Entry],
|
||||||
|
][]) {
|
||||||
|
const upConfig = getNavigationTarget(nav.up);
|
||||||
|
if (upConfig?.target === currentButton) {
|
||||||
|
targetButton = button;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const targetNav = navigation[downConfig.target];
|
const targetNav = navigation[downConfig.target];
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import gsap from "gsap";
|
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 = [
|
export const SETTINGS_MENUS = [
|
||||||
"options",
|
"options",
|
||||||
"clock",
|
"clock",
|
||||||
@@ -80,23 +83,9 @@ export const useSettingsStore = defineStore("settings", {
|
|||||||
{ offsetY: 0, opacity: 1, duration: 0.25, ease: "none" },
|
{ offsetY: 0, opacity: 1, duration: 0.25, ease: "none" },
|
||||||
"+=0.05",
|
"+=0.05",
|
||||||
)
|
)
|
||||||
.to(this, {
|
|
||||||
submenuButtonsOffsetY: 24,
|
|
||||||
duration: 0.167,
|
|
||||||
ease: "none",
|
|
||||||
})
|
|
||||||
.call(() => {
|
.call(() => {
|
||||||
this.currentSubMenu = submenu;
|
this.currentSubMenu = submenu;
|
||||||
})
|
});
|
||||||
.to(
|
|
||||||
this,
|
|
||||||
{
|
|
||||||
submenuButtonsOffsetY: 0,
|
|
||||||
duration: 0.167,
|
|
||||||
ease: "none",
|
|
||||||
},
|
|
||||||
"+=0.05",
|
|
||||||
);
|
|
||||||
|
|
||||||
const achievements = useAchievementsStore();
|
const achievements = useAchievementsStore();
|
||||||
if (!achievements.advancement.visitedSettings.includes(submenu)) {
|
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 |