From a31f72f41dc45190a83e6f1d6c705f3262247687 Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Tue, 16 Dec 2025 12:17:49 +0100 Subject: [PATCH] feat(projects): animate buttons on click --- .../projects/bottom-screen/circle_big.webp | Bin 0 -> 392 bytes .../projects/bottom-screen/circle_small.webp | Bin 0 -> 376 bytes .../projects/bottom-screen/link-pressed.webp | Bin 0 -> 198 bytes .../projects/bottom-screen/next-pressed.webp | Bin 0 -> 174 bytes .../projects/bottom-screen/prev-pressed.webp | Bin 0 -> 178 bytes .../projects/bottom-screen/quit-pressed.webp | Bin 0 -> 178 bytes .../Projects/BottomScreen/Buttons.vue | 130 ++++++++++++++++-- 7 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 app/assets/images/projects/bottom-screen/circle_big.webp create mode 100644 app/assets/images/projects/bottom-screen/circle_small.webp create mode 100644 app/assets/images/projects/bottom-screen/link-pressed.webp create mode 100644 app/assets/images/projects/bottom-screen/next-pressed.webp create mode 100644 app/assets/images/projects/bottom-screen/prev-pressed.webp create mode 100644 app/assets/images/projects/bottom-screen/quit-pressed.webp diff --git a/app/assets/images/projects/bottom-screen/circle_big.webp b/app/assets/images/projects/bottom-screen/circle_big.webp new file mode 100644 index 0000000000000000000000000000000000000000..6ec7a2db8cf088ace4b2ab2de1a66d0548d24052 GIT binary patch literal 392 zcmV;30eAjVNk&G10RRA3MM6+kP&iC<0RR9mSHKkz7a$f)cIJ-|wLl0!jXA zL##6Y0oNWN$F|+J0TxsTj@76H2RZ#j}jtF~X^ro(hUvd>~RFUhB6b1FJh2oLC~MnyHnrkA944mP$E%SXb{F8W8kfC2Y3gu%_9VByGxqZD8o@$iqp zerfiveC>bzTD`D}`&!Mis%I^PwIph8y@pW=%{lrDbZ>bP_OyV#_gH08>9paVGFIF4x24YL^~=_V337!Cx&Ef!M} zb<#q0qVCM3ZPlF!;Un3B|3_*!Le9rZ&JW~#y;k2+W8GGIa+_sG)AyBXF zA+aRMFgGTj^#G4_n`i*acCKtzvQ3Jz^{R`fF-sH>>%o)sEqZyy3XCB5liMM+vd z>8)+=40=XHQA#_y_bVL@u>K?sR6V7gd476_bd1ILNf9#|Q)zaprB9qjWKUlO09S)m AHvj+t literal 0 HcmV?d00001 diff --git a/app/assets/images/projects/bottom-screen/next-pressed.webp b/app/assets/images/projects/bottom-screen/next-pressed.webp new file mode 100644 index 0000000000000000000000000000000000000000..f98840859879405b963a06dcbc281dd0363d506d GIT binary patch literal 174 zcmV;f08#%^Nk&Gd00012MM6+kP&iDQ0000l8vq9oCqO1`Q~wHk_8e10q-`_*&gNk7 z@G=Oqjm9{#A>Q*a=05<|Hv3({K_p31)I0_j1-lIj7zBLn-_$%8=OCj0697+=NISzh z=uBtFB7k0*e`B~gq9?=6S<6~D9J9ta8Gm7IBz|kGjo?V=-z0ujllICfU6oIgWv6sY cdvtzUAzkL|lYm>2nVF;&0FQ*a=05<|Hv3({K^sYq)E5ZsBG~VH1UCmDeDfzYVf73m`ac2iB#Cq| zT!X=Mbu0oHm359JdqfP^lGXKz7JkT;WH0#kmp;Ln-dHbuT~FR1e^rx?$|+rypCrpp g>6VV@{Aq`DnX{h++>*@9B<%p8bceqa#J7V701c>4(f|Me literal 0 HcmV?d00001 diff --git a/app/assets/images/projects/bottom-screen/quit-pressed.webp b/app/assets/images/projects/bottom-screen/quit-pressed.webp new file mode 100644 index 0000000000000000000000000000000000000000..edd09d5ec93a9250ffe483bf95f9adaadd2d987f GIT binary patch literal 178 zcmV;j08Rf=Nk&Gh00012MM6+kP&iDU0000l8vq9oCqSfaGad6UX7KRRgam~9-ijh& zw$a!|K7@N7&in_UyXTz>4kFo(qR(L}|EB)`q1Hh}|0lpafTfLa4jQ$D zvm?XI+Q=M)HDCm5z?RJo>W4A{wfM~e +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 PREV_BUTTON: Point = [36, 100]; -const QUIT_BUTTON: Point = [88, 156]; -const LINK_BUTTON: Point = [168, 156]; -const NEXT_BUTTON: Point = [220, 100]; +const [ + prevPressedImage, + quitPressedImage, + 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 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 = ( [cx, cy]: Point, [x, y]: Point, radius: number, ): 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) => { + if (currentAnimation) return; + 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"); - } else if (circleContains(NEXT_BUTTON, [x, y], CLICK_RADIUS)) { + } else if (circleContains(BUTTONS.next.position, [x, y], CLICK_RADIUS)) { + startButtonAnimation("next"); 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"); } else if ( - circleContains(LINK_BUTTON, [x, y], CLICK_RADIUS) && + circleContains(BUTTONS.link.position, [x, y], CLICK_RADIUS) && project?.link ) { + startButtonAnimation("link"); // TODO: show confirmation popup before opening the link, like "you are about to navigate to [...]" - store.visitProject(); + // store.visitProject(); } }); -useScreenMouseWheel((dy) => { - if (dy > 0) { - store.scrollProjects("right"); - } else if (dy < 0) { - store.scrollProjects("left"); +useRender((ctx) => { + if (!currentAnimation) return; + + if (currentAnimation.showButton) { + 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) => { + if (currentAnimation) return; switch (key) { case "NDS_LEFT": + startButtonAnimation("prev"); store.scrollProjects("left"); break; case "NDS_RIGHT": + startButtonAnimation("next"); store.scrollProjects("right"); break; }