diff --git a/app/components/Common/ButtonSelector.vue b/app/components/Common/ButtonSelector.vue index 57a90a5..39b1d26 100644 --- a/app/components/Common/ButtonSelector.vue +++ b/app/components/Common/ButtonSelector.vue @@ -10,39 +10,10 @@ const props = withDefaults( ); const { onRender } = useScreen(); - const { assets } = useAssets(); -const ANIMATION_SPEED = 15; - -let [currentX, currentY, currentWidth, currentHeight] = props.rect; - -onRender((ctx, deltaTime) => { - const [targetX, targetY, targetWidth, targetHeight] = props.rect; - const dx = targetX - currentX; - const dy = targetY - currentY; - const dw = targetWidth - currentWidth; - const dh = targetHeight - currentHeight; - - if ( - Math.abs(dx) < 0.5 && - Math.abs(dy) < 0.5 && - Math.abs(dw) < 0.5 && - Math.abs(dh) < 0.5 - ) { - [currentX, currentY, currentWidth, currentHeight] = props.rect; - } else { - const speed = (ANIMATION_SPEED * deltaTime) / 1000; - currentX += dx * speed; - currentY += dy * speed; - currentWidth += dw * speed; - currentHeight += dh * speed; - } - - const x = Math.floor(currentX); - const y = Math.floor(currentY); - const w = Math.floor(currentWidth); - const h = Math.floor(currentHeight); +onRender((ctx) => { + const [x, y, w, h] = props.rect; ctx.globalAlpha = props.opacity; diff --git a/app/composables/useButtonNavigation.ts b/app/composables/useButtonNavigation.ts index 0a6852d..1ff8350 100644 --- a/app/composables/useButtonNavigation.ts +++ b/app/composables/useButtonNavigation.ts @@ -1,6 +1,6 @@ -export type ButtonConfig = [x: number, y: number, w: number, h: number]; +import gsap from "gsap"; -export const useButtonNavigation = >({ +export const useButtonNavigation = >({ buttons, initialButton, onButtonClick, @@ -22,27 +22,160 @@ export const useButtonNavigation = >({ >; disabled?: Ref; }) => { + type Entry = keyof T; + const confirmationModal = useConfirmationModal(); const selectedButton = ref(initialButton); - const selectorPosition = computed(() => buttons[selectedButton.value]!); + const selectorPosition = ref(buttons[initialButton]!); + const nextButton = ref(); - const nextButton = ref(); + const buildNavigationGraph = ( + nav: typeof navigation, + ): Map> => { + const edges = new Map>(); + + for (const [button, navConfig] of Object.entries(nav) as [ + Entry, + (typeof nav)[Entry], + ][]) { + if (!edges.has(button)) { + edges.set(button, new Set()); + } + + if (navConfig.up && navConfig.up !== "last") { + edges.get(button)!.add(navConfig.up); + } + if (navConfig.down && navConfig.down !== "last") { + edges.get(button)!.add(navConfig.down); + } + if (navConfig.left) { + edges.get(button)!.add(navConfig.left); + } + if (navConfig.right) { + edges.get(button)!.add(navConfig.right); + } + + if (navConfig.up === "last" || navConfig.down === "last") { + for (const [otherButton, otherNav] of Object.entries(nav) as [ + Entry, + (typeof nav)[Entry], + ][]) { + if (otherButton === button) continue; + + if ( + otherNav.up === button || + otherNav.down === button || + otherNav.left === button || + otherNav.right === button + ) { + edges.get(button)!.add(otherButton); + } + } + } + } + + return edges; + }; + + const findPath = ( + graph: Map>, + from: Entry, + to: Entry, + ): Array | null => { + if (from === to) return []; + + const queue: Array<{ button: Entry; path: Array }> = [ + { button: from, path: [] }, + ]; + const visited = new Set([from]); + + while (queue.length > 0) { + const { button, path } = queue.shift()!; + + const neighbors = graph.get(button); + if (!neighbors) continue; + + for (const neighbor of neighbors) { + if (visited.has(neighbor)) continue; + + const newPath = [...path, neighbor]; + + if (neighbor === to) { + return newPath; + } + + visited.add(neighbor); + queue.push({ button: neighbor, path: newPath }); + } + } + + return null; + }; + + const graph = buildNavigationGraph(navigation); + + const calculateDistance = ( + [x1, y1, w1, h1]: Rect, + [x2, y2, w2, h2]: Rect, + ): number => { + const cx1 = x1 + w1 / 2; + const cy1 = y1 + h1 / 2; + const cx2 = x2 + w2 / 2; + const cy2 = y2 + h2 / 2; + + return Math.sqrt((cx2 - cx1) ** 2 + (cy2 - cy1) ** 2); + }; + + const animateToButton = (targetButton: Entry, path?: Array | null) => { + const SPEED = 400; + const pathButtons = path && path.length > 0 ? path : [targetButton]; + + const timeline = gsap.timeline(); + let prevRect = selectorPosition.value; + + selectorPosition.value = [...selectorPosition.value]; + + for (const button of pathButtons) { + const buttonRect = buttons[button]!; + const distance = calculateDistance(prevRect, buttonRect); + const duration = distance / SPEED; + + timeline.to( + selectorPosition.value, + { + [0]: buttonRect[0], + [1]: buttonRect[1], + [2]: buttonRect[2], + [3]: buttonRect[3], + duration, + ease: "none", + }, + "+=0", + ); + + prevRect = buttonRect; + } + + selectedButton.value = targetButton; + }; const { onClick } = useScreen(); onClick((x: number, y: number) => { if (confirmationModal.isOpen || disabled?.value) return; - for (const [buttonName, config] of Object.entries(buttons) as [ - keyof T, - ButtonConfig, + for (const [buttonName, buttonRect] of Object.entries(buttons) as [ + Entry, + Rect, ][]) { - const [sx, sy, sw, sh] = config; + const [sx, sy, sw, sh] = buttonRect; if (x >= sx && x <= sx + sw && y >= sy && y <= sy + sh) { if (selectedButton.value === buttonName) { onButtonClick?.(buttonName); } else { + const path = findPath(graph, selectedButton.value, buttonName); + if ( (navigation[buttonName].down === "last" && navigation[selectedButton.value]!.up === buttonName) || @@ -52,7 +185,7 @@ export const useButtonNavigation = >({ nextButton.value = selectedButton.value; } - selectedButton.value = buttonName; + animateToButton(buttonName, path); } break; } @@ -62,26 +195,28 @@ export const useButtonNavigation = >({ useKeyDown((key) => { if (confirmationModal.isOpen || disabled?.value) return; - const currentButton = selectedButton.value as keyof T; + const currentButton = selectedButton.value as Entry; const currentNav = navigation[currentButton]; if (!currentNav) return; + let targetButton: Entry | undefined; + switch (key) { case "NDS_UP": if (!currentNav.up) return; if (currentNav.up === "last") { if (nextButton.value) { - selectedButton.value = nextButton.value; + targetButton = nextButton.value; } else { - selectedButton.value = currentNav.left ?? currentNav.right; + targetButton = currentNav.left ?? currentNav.right; } } else { if (navigation[currentNav.up].down === "last") { - nextButton.value = selectedButton.value as keyof T; + nextButton.value = selectedButton.value as Entry; } - selectedButton.value = currentNav.up; + targetButton = currentNav.up; } break; @@ -91,15 +226,15 @@ export const useButtonNavigation = >({ if (currentNav.down === "last") { if (nextButton.value) { - selectedButton.value = nextButton.value; + targetButton = nextButton.value; } else { - selectedButton.value = currentNav.left ?? currentNav.right; + targetButton = currentNav.left ?? currentNav.right; } } else { if (navigation[currentNav.down].up === "last") { - nextButton.value = selectedButton.value as keyof T; + nextButton.value = selectedButton.value as Entry; } - selectedButton.value = currentNav.down; + targetButton = currentNav.down; } break; @@ -109,7 +244,7 @@ export const useButtonNavigation = >({ if (currentNav.horizontalMode === "preview") { nextButton.value = currentNav.left; } else { - selectedButton.value = currentNav.left; + targetButton = currentNav.left; } break; @@ -119,7 +254,7 @@ export const useButtonNavigation = >({ if (currentNav.horizontalMode === "preview") { nextButton.value = currentNav.right; } else { - selectedButton.value = currentNav.right; + targetButton = currentNav.right; } break; @@ -131,6 +266,10 @@ export const useButtonNavigation = >({ default: return; } + + if (targetButton) { + animateToButton(targetButton, null); + } }); return {