explodorama / files / src / ui / padPlaces.ts
padPlaces.ts
92cc4ee3 2026-11-02 Pavel Safronov | 1 | import { PAD_LAYOUTS, type PadDot, type PadLayout } from './padLayouts' |
| 2 | import { faceOf, type FaceIndex, type PadFamily } from './padMap' | |
| 3 | ||
| 4 | /** | |
| 5 | * Where each part of the demo's virtual controller (padOverlay.ts) goes for a pad family, from the | |
| 6 | * measured layouts (padLayouts.ts). Pure, so the tests can check it without a DOM. | |
| 7 | */ | |
| 8 | export type PadSlot = 'dpad' | 'stick' | 'move' | 'bomb' | 'charge' | 'wait' | 'l1' | 'r1' | 'start' | |
| 9 | ||
| 10 | /** A part's picture: its centre and its size (a square), in the layout's units. */ | |
| 11 | export interface PadPlace { x: number; y: number; size: number } | |
| 12 | ||
| 13 | /** The face position (a key of `layout.face`) of each raw face index: 0 bottom, 1 right, 2 left, 3 top. */ | |
| 14 | const FACE_POSITION = ['bottom', 'right', 'left', 'top'] as const satisfies readonly (keyof PadLayout['face'])[] | |
| 15 | ||
| 16 | /** How much bigger than its button's circle a picture is drawn, so the picture covers the circle. */ | |
| 17 | export const GLYPH_SCALE = 1.2 | |
| 18 | ||
| 19 | /** A pad that is none of the three has the Xbox layout (W3C standard mapping). */ | |
| 20 | export function layoutOf(family: PadFamily): PadLayout { | |
| 21 | return PAD_LAYOUTS[family === 'generic' ? 'xbox' : family] | |
| 22 | } | |
| 23 | ||
| 24 | /** The face position of `action` on a pad of `family`, e.g. Nintendo's Move is 'right'. */ | |
| 25 | export function facePosition(family: PadFamily, action: 'move' | 'bomb' | 'charge' | 'wait'): keyof PadLayout['face'] { | |
| 26 | return FACE_POSITION[faceOf(family, action) as FaceIndex] | |
| 27 | } | |
| 28 | ||
| 29 | const place = (d: PadDot, scale = GLYPH_SCALE): PadPlace => ({ x: d.x, y: d.y, size: d.r * 2 * scale }) | |
| 30 | ||
| 31 | /** Every lit part's place on the pad of `family`. */ | |
| 32 | export function padPlaces(family: PadFamily): Record<PadSlot, PadPlace> { | |
| 33 | const l = layoutOf(family) | |
| 34 | const face = (a: 'move' | 'bomb' | 'charge' | 'wait'): PadPlace => place(l.face[facePosition(family, a)]) | |
| 35 | const shoulder = (b: PadLayout['l']): PadPlace => ({ x: b.x, y: b.y, size: b.h * 2.4 }) | |
| 36 | return { | |
| 37 | dpad: place(l.dpad), | |
| 38 | stick: place(l.rstick), | |
| 39 | move: face('move'), | |
| 40 | bomb: face('bomb'), | |
| 41 | charge: face('charge'), | |
| 42 | wait: face('wait'), | |
| 43 | l1: shoulder(l.l), | |
| 44 | r1: shoulder(l.r), | |
| 45 | start: place(l.start, 1.5), | |
| 46 | } | |
| 47 | } |