explodorama / files / tools / make-pad-layouts.py
make-pad-layouts.py
| 1 | #!/usr/bin/env python3 |
| 2 | """Writes assets/controller-art/*.png and src/ui/padLayouts.ts: the controller pictures of the demo's |
| 3 | virtual controller, and where each button sits on them. |
| 4 | |
| 5 | The pictures are line drawings by their maker (OpenGameArt "Controller style 1/3/5", CC BY 4.0; see |
| 6 | README Attribution and docs/art-provenance.md). This script scales each to 1200 px wide, trims the |
| 7 | empty border (plus a small margin) and saves it, then writes where the buttons are. The positions below |
| 8 | were measured by eye on the drawings at 800 px wide (3000 px originals / 3.75); the script converts them |
| 9 | to the trimmed picture's own units. |
| 10 | Needs ImageMagick (`magick`) and the downloaded originals in SRC (outside the repository). |
| 11 | Run: python3 tools/make-pad-layouts.py |
| 12 | """ |
| 13 | import json, os, subprocess |
| 14 | |
| 15 | SRC = os.path.expanduser('~/code/controller-pack') |
| 16 | FRAME = 800 # width the measurements below were taken at |
| 17 | MARGIN = 8 # px of empty border kept round the drawing, at 1200 px wide |
| 18 | |
| 19 | # part: (x, y, r) face: top / left / right / bottom by position on the pad. l / r: (x, y, w, h) |
| 20 | FAMILIES = { |
| 21 | 'xbox': { # Controller style 1 (an Xbox-style pad); generic pads use it too |
| 22 | 'file': 'controller_1_v2.png', |
| 23 | 'dpad': (320, 283, 52), 'rstick': (481, 279, 52), 'start': (445, 184, 15), |
| 24 | 'face': {'top': (560, 141, 21), 'left': (517, 184, 21), 'right': (603, 184, 21), 'bottom': (560, 227, 21)}, |
| 25 | 'l': (235, 78, 50, 16), 'r': (565, 78, 50, 16), |
| 26 | }, |
| 27 | 'playstation': { # Controller style 5 (a DualSense-style pad) |
| 28 | 'file': 'controller_5.png', |
| 29 | 'dpad': (198, 172, 54), 'rstick': (504, 261, 40), 'start': (551, 100, 11), |
| 30 | 'face': {'top': (602, 125, 22), 'left': (555, 172, 22), 'right': (650, 172, 22), 'bottom': (602, 219, 22)}, |
| 31 | 'l': (178, 54, 40, 12), 'r': (622, 54, 40, 12), |
| 32 | }, |
| 33 | 'nintendo': { # Controller style 3 (a Switch Pro-style pad) |
| 34 | 'file': 'controller_3.png', |
| 35 | 'dpad': (305, 265, 45), 'rstick': (481, 265, 36), 'start': (477, 134, 13), |
| 36 | 'face': {'top': (563, 139, 21), 'left': (514, 181, 21), 'right': (611, 181, 21), 'bottom': (563, 222, 21)}, |
| 37 | 'l': (215, 80, 60, 14), 'r': (585, 80, 60, 14), |
| 38 | }, |
| 39 | } |
| 40 | |
| 41 | def run(*a): |
| 42 | return subprocess.run(a, check=True, capture_output=True, text=True).stdout.strip() |
| 43 | |
| 44 | out = {} |
| 45 | here = os.path.dirname(os.path.abspath(__file__)) |
| 46 | for name, f in FAMILIES.items(): |
| 47 | src = os.path.join(SRC, f['file']) |
| 48 | k = 1200 / FRAME |
| 49 | # the drawing's box at 1200 px wide: "WxH+X+Y" |
| 50 | box = run('magick', src, '-resize', '1200x', '-format', '%@', 'info:') |
| 51 | size, x, y = box.split('+')[0], int(box.split('+')[1]), int(box.split('+')[2]) |
| 52 | bw, bh = (int(v) for v in size.split('x')) |
| 53 | x0, y0, w, h = x - MARGIN, y - MARGIN, bw + 2 * MARGIN, bh + 2 * MARGIN |
| 54 | dest = f'padart-{name}.png' |
| 55 | run('magick', src, '-resize', '1200x', '-crop', f'{w}x{h}+{x0}+{y0}', '+repage', '-strip', os.path.join(here, '..', 'assets', 'controller-art', dest)) |
| 56 | u = 1000 / w |
| 57 | def pt(px, py): return round((px * k - x0) * u, 1), round((py * k - y0) * u, 1) |
| 58 | def part(px, py, r): a, b = pt(px, py); return {'x': a, 'y': b, 'r': round(r * k * u, 1)} |
| 59 | def boxp(px, py, bw2, bh2): a, b = pt(px, py); return {'x': a, 'y': b, 'w': round(bw2 * k * u, 1), 'h': round(bh2 * k * u, 1)} |
| 60 | out[name] = { |
| 61 | 'image': dest, 'width': 1000, 'height': round(h * u, 1), |
| 62 | 'dpad': part(*f['dpad']), 'rstick': part(*f['rstick']), 'start': part(*f['start']), |
| 63 | 'face': {key: part(*v) for key, v in f['face'].items()}, |
| 64 | 'l': boxp(*f['l']), 'r': boxp(*f['r']), |
| 65 | } |
| 66 | |
| 67 | ts = '''/** |
| 68 | * Where the buttons sit on each controller family's picture (assets/controller-art/), generated by |
| 69 | * tools/make-pad-layouts.py from measurements of the drawings; do not edit by hand. The pictures are |
| 70 | * line drawings from OpenGameArt's "Controller style 1 / 3 / 5", CC BY 4.0 (see README Attribution). |
| 71 | * Units: a box `width` x `height` (1000 wide) covering the picture; `x`, `y` are the centre of a |
| 72 | * part, `r` its radius, `w` and `h` a shoulder button's size. Generic pads use the Xbox one. |
| 73 | */ |
| 74 | export interface PadDot { x: number; y: number; r: number } |
| 75 | export interface PadBox { x: number; y: number; w: number; h: number } |
| 76 | export interface PadLayout { |
| 77 | /** The picture's file name in assets/controller-art/. */ |
| 78 | image: string |
| 79 | width: number |
| 80 | height: number |
| 81 | dpad: PadDot |
| 82 | rstick: PadDot |
| 83 | /** Start / Options / Menu / Plus. */ |
| 84 | start: PadDot |
| 85 | /** The four face buttons by their place on the pad. */ |
| 86 | face: { top: PadDot; left: PadDot; right: PadDot; bottom: PadDot } |
| 87 | l: PadBox |
| 88 | r: PadBox |
| 89 | } |
| 90 | |
| 91 | export const PAD_LAYOUTS: Record<'xbox' | 'playstation' | 'nintendo', PadLayout> = ''' + json.dumps(out, indent=2) + '\n' |
| 92 | open(os.path.join(here, '..', 'src', 'ui', 'padLayouts.ts'), 'w').write(ts) |
| 93 | print('ok', {k: (v['width'], v['height']) for k, v in out.items()}) |