feat(projects): animate buttons on click

This commit is contained in:
2025-12-16 12:17:49 +01:00
parent b1040b5dd3
commit 05398d5252
7 changed files with 116 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

View File

@@ -1,50 +1,152 @@
<script setup lang="ts"> <script setup lang="ts">
import PREV_PRESSED_IMAGE from "~/assets/images/projects/bottom-screen/prev-pressed.webp";
import QUIT_PRESSED_IMAGE from "~/assets/images/projects/bottom-screen/quit-pressed.webp";
import LINK_PRESSED_IMAGE from "~/assets/images/projects/bottom-screen/link-pressed.webp";
import NEXT_PRESSED_IMAGE from "~/assets/images/projects/bottom-screen/next-pressed.webp";
import CIRCLE_SMALL_IMAGE from "~/assets/images/projects/bottom-screen/circle_small.webp";
import CIRCLE_BIG_IMAGE from "~/assets/images/projects/bottom-screen/circle_big.webp";
import gsap from "gsap";
const store = useProjectsStore(); const store = useProjectsStore();
const PREV_BUTTON: Point = [36, 100]; const [
const QUIT_BUTTON: Point = [88, 156]; prevPressedImage,
const LINK_BUTTON: Point = [168, 156]; quitPressedImage,
const NEXT_BUTTON: Point = [220, 100]; linkPressedImage,
nextPressedImage,
circleSmallImagee,
circleBigImage,
] = useImages(
PREV_PRESSED_IMAGE,
QUIT_PRESSED_IMAGE,
LINK_PRESSED_IMAGE,
NEXT_PRESSED_IMAGE,
CIRCLE_SMALL_IMAGE,
CIRCLE_BIG_IMAGE,
);
const CLICK_RADIUS = 22; const CLICK_RADIUS = 22;
const BUTTONS = {
prev: { position: [36, 100], image: prevPressedImage! },
quit: { position: [88, 156], image: quitPressedImage! },
link: { position: [168, 156], image: linkPressedImage! },
next: { position: [220, 100], image: nextPressedImage! },
} as const satisfies Record<
string,
{ position: Point; image: HTMLImageElement }
>;
type ButtonType = keyof typeof BUTTONS;
type ButtonAnimation = {
type: ButtonType;
position: Point;
showButton: boolean;
showSmallCircle: boolean;
showBigCircle: boolean;
};
let currentAnimation: ButtonAnimation | null = null;
const circleContains = ( const circleContains = (
[cx, cy]: Point, [cx, cy]: Point,
[x, y]: Point, [x, y]: Point,
radius: number, radius: number,
): boolean => Math.sqrt(Math.pow(x - cx, 2) + Math.pow(y - cy, 2)) < radius; ): boolean => Math.sqrt(Math.pow(x - cx, 2) + Math.pow(y - cy, 2)) < radius;
const startButtonAnimation = (type: ButtonType) => {
const anim: ButtonAnimation = {
type,
position: BUTTONS[type].position,
showButton: true,
showSmallCircle: true,
showBigCircle: false,
};
currentAnimation = anim;
const SMALL_CIRCLE_DURATION = 0.07;
const BIG_CIRCLE_DURATION = 0.09;
const POST_DURATION = 0.07;
gsap
.timeline()
.to(anim, { duration: SMALL_CIRCLE_DURATION })
.call(() => {
anim.showSmallCircle = false;
anim.showBigCircle = true;
})
.to(anim, { duration: BIG_CIRCLE_DURATION })
.call(() => {
anim.showBigCircle = false;
})
.to(anim, { duration: POST_DURATION })
.call(() => {
currentAnimation = null;
});
};
useScreenClick((x, y) => { useScreenClick((x, y) => {
if (currentAnimation) return;
const project = store.projects[store.currentProject]; const project = store.projects[store.currentProject];
if (circleContains(PREV_BUTTON, [x, y], CLICK_RADIUS)) { if (circleContains(BUTTONS.prev.position, [x, y], CLICK_RADIUS)) {
startButtonAnimation("prev");
store.scrollProjects("left"); store.scrollProjects("left");
} else if (circleContains(NEXT_BUTTON, [x, y], CLICK_RADIUS)) { } else if (circleContains(BUTTONS.next.position, [x, y], CLICK_RADIUS)) {
startButtonAnimation("next");
store.scrollProjects("right"); store.scrollProjects("right");
} else if (circleContains(QUIT_BUTTON, [x, y], CLICK_RADIUS)) { } else if (circleContains(BUTTONS.quit.position, [x, y], CLICK_RADIUS)) {
startButtonAnimation("quit");
throw new Error("quit"); throw new Error("quit");
} else if ( } else if (
circleContains(LINK_BUTTON, [x, y], CLICK_RADIUS) && circleContains(BUTTONS.link.position, [x, y], CLICK_RADIUS) &&
project?.link project?.link
) { ) {
startButtonAnimation("link");
// TODO: show confirmation popup before opening the link, like "you are about to navigate to [...]" // TODO: show confirmation popup before opening the link, like "you are about to navigate to [...]"
store.visitProject(); // store.visitProject();
} }
}); });
useScreenMouseWheel((dy) => { useRender((ctx) => {
if (dy > 0) { if (!currentAnimation) return;
store.scrollProjects("right");
} else if (dy < 0) { if (currentAnimation.showButton) {
store.scrollProjects("left"); const image = BUTTONS[currentAnimation.type].image;
ctx.drawImage(
image!,
currentAnimation.position[0] - 14,
currentAnimation.position[1] - 14,
);
}
if (currentAnimation.showSmallCircle) {
ctx.drawImage(
circleSmallImagee!,
currentAnimation.position[0] - 28,
currentAnimation.position[1] - 28,
);
}
if (currentAnimation.showBigCircle) {
ctx.drawImage(
circleBigImage!,
currentAnimation.position[0] - 44,
currentAnimation.position[1] - 44,
);
} }
}); });
useKeyDown((key) => { useKeyDown((key) => {
if (currentAnimation) return;
switch (key) { switch (key) {
case "NDS_LEFT": case "NDS_LEFT":
startButtonAnimation("prev");
store.scrollProjects("left"); store.scrollProjects("left");
break; break;
case "NDS_RIGHT": case "NDS_RIGHT":
startButtonAnimation("next");
store.scrollProjects("right"); store.scrollProjects("right");
break; break;
} }