fuzzyworld

explodorama / files / src / ui / spatial.ts

spatial.ts

1.8 KB · history · blame

1/**
2 * Moving focus between on-screen controls with a d-pad or stick (controller support, stage 1):
3 * the neighbour in the pressed direction, judged by where the rectangles are. Pure, so the caller
4 * measures the elements (`getBoundingClientRect`) and this only does the geometry.
5 */
6/** One step along an axis: back, none, forward. */
7export type Step = -1 | 0 | 1
8
9export interface FocusRect {
10 x: number
11 y: number
12 w: number
13 h: number
14}
15
16/**
17 * The index of the best rectangle in direction (dx, dy) from `rects[current]`, or `current` if
18 * none lies that way. A candidate counts when its centre is strictly past the current centre on
19 * every axis the direction uses. Score = distance along the direction + 2 x distance sideways
20 * (lower wins; a tie goes to the lower index), so a button straight along the row beats a nearer
21 * one a row off. An empty list gives -1; a `current` that is -1 or out of range gives 0; (0, 0)
22 * gives `current`. For a diagonal, both axes count as "along" and nothing is sideways.
23 */
24export function nextFocus(rects: FocusRect[], current: number, dx: Step, dy: Step): number {
25 if (rects.length === 0) return -1
26 const cur = rects[current]
27 if (cur === undefined) return 0
28 const cx = cur.x + cur.w / 2
29 const cy = cur.y + cur.h / 2
30 if (dx === 0 && dy === 0) return current
31 let best = current
32 let bestScore = Infinity
33 rects.forEach((r, i) => {
34 if (i === current) return
35 const ox = r.x + r.w / 2 - cx
36 const oy = r.y + r.h / 2 - cy
37 if (dx !== 0 && ox * dx <= 0) return
38 if (dy !== 0 && oy * dy <= 0) return
39 const along = (dx !== 0 ? Math.abs(ox) : 0) + (dy !== 0 ? Math.abs(oy) : 0)
40 const side = (dx === 0 ? Math.abs(ox) : 0) + (dy === 0 ? Math.abs(oy) : 0)
41 const score = along + 2 * side
42 if (score < bestScore) {
43 bestScore = score
44 best = i
45 }
46 })
47 return best
48}