fuzzyworld

explodorama / files / tools / make-pad-layouts.py

make-pad-layouts.py

Blame: the commit that last changed each line · file · history

92cc4ee3 2026-11-02 Pavel Safronov1#!/usr/bin/env python3
2"""Writes assets/controller-art/*.png and src/ui/padLayouts.ts: the controller pictures of the demo's
3virtual controller, and where each button sits on them.
4
5The pictures are line drawings by their maker (OpenGameArt "Controller style 1/3/5", CC BY 4.0; see
6README Attribution and docs/art-provenance.md). This script scales each to 1200 px wide, trims the
7empty border (plus a small margin) and saves it, then writes where the buttons are. The positions below
8were measured by eye on the drawings at 800 px wide (3000 px originals / 3.75); the script converts them
9to the trimmed picture's own units.
10Needs ImageMagick (`magick`) and the downloaded originals in SRC (outside the repository).
11Run: python3 tools/make-pad-layouts.py
12"""
13import json, os, subprocess
14
15SRC = os.path.expanduser('~/code/controller-pack')
16FRAME = 800 # width the measurements below were taken at
17MARGIN = 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)
20FAMILIES = {
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
41def run(*a):
42 return subprocess.run(a, check=True, capture_output=True, text=True).stdout.strip()
43
44out = {}
45here = os.path.dirname(os.path.abspath(__file__))
46for 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
67ts = '''/**
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 */
74export interface PadDot { x: number; y: number; r: number }
75export interface PadBox { x: number; y: number; w: number; h: number }
76export 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
91export const PAD_LAYOUTS: Record<'xbox' | 'playstation' | 'nintendo', PadLayout> = ''' + json.dumps(out, indent=2) + '\n'
92open(os.path.join(here, '..', 'src', 'ui', 'padLayouts.ts'), 'w').write(ts)
93print('ok', {k: (v['width'], v['height']) for k, v in out.items()})