Files
NX2navi/launcher/ui/art.py
Claude c403547c8b Add the NX2navi launcher: a one-click setup that builds the game from the player's dump, starts it and keeps it up to date.
NX2navi-Setup.exe installs the launcher into %LOCALAPPDATA%\Programs\NX2navi without administrator rights, with
shortcuts and an Apps entry. The launcher finds the player's .nup, downloads the build tools itself (the MSYS2 UCRT64
packages toolchain.lock.json pins, checked by sha256 and unpacked without pacman, and zbic from PyPI) and runs
tools/make_game.py, whose steps it shows with their progress. Its window follows the game's options menu: colours
sampled from it, the menu's filigree, the white glowing cursor, the Triforce in the title band and as the programs'
icon, and all text in the game's Cetra Display, decoded from the player's own dump. Mouse, keyboard and an XInput
pad drive it.

Updates come from GitHub releases (NX2navi-update.json): the new version is downloaded, checked and staged beside
the running one, the stub restarts into it and falls back to the previous version when it fails to start, and the
game is rebuilt without the dump. make_game.py now keys its exefs and romfs steps on the game files alone, has
--rebuild and --events, compiles only the files whose source, headers, flags or compiler changed, and replaces
ocarina.exe only once the new one links. tools/make_launcher.py builds the release on Windows or Linux.

Tested: tests/launcher_test.py (19 tests), the real toolchain download and install, and under Wine 11 the setup,
the stub, the window on the bundled Windows Python, an update from a local feed by clicking Updates, the rollback
of a version that fails to start, and the downloaded clang compiling and linking a program against the game's
libraries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merged onto the current port: make_game.py keeps the launcher's step keys, cached link and in-place swap, and its
own runtime manifest (windres), AMD's FidelityFX library and the shader preparation, which becomes a step of its own
("shaders") that the launcher shows in its install, rebuild and update jobs. make_launcher.py ships the shader tools
and, with --ffx, AMD's library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:50:52 -05:00

539 lines
23 KiB
Python

"""The window's pictures, drawn with numpy into RGBA arrays and handed to Tk as PNG data.
Everything is drawn here from shapes and colours: the misty green backdrop, the wooden title band, the bronze label
cells with their mottled texture, the description box, the white glowing cursor, the filigree divider, the Triforce
and the small icons. Tk's canvas draws lines and polygons without antialiasing on Windows, so every curved or soft
shape is a picture; flat rectangles and text stay canvas items.
Colours follow the game's options menu (docs/launcher.md, "Look"). Sizes are in logical pixels times the display
scale s.
"""
import base64
import re
import struct
import zlib
import numpy as np
# ---------------------------------------------------------------- colours
PALETTE = {
'window': '#06180d',
'panel': '#3a2b22',
'label_top': '#7a5c32', 'label_mid': '#775930', 'label_bottom': '#72552d', 'label_border': '#6a4f29',
'ring': '#2a2019',
'value': '#2f2d22', 'value_border': '#45402f',
'orange': '#d69127', 'orange_light': '#e3a234', 'orange_dark': '#8f5f17',
'desc': '#403024', 'desc_border': '#4f3f30',
'text': '#e9dcc0', 'text_bright': '#f1e6cc', 'value_text': '#e3d5b6', 'desc_text': '#e9dfc8',
'muted': '#d4c6a6', 'dim': '#7d786a', 'off': '#5e5b51', 'on_text': '#fdf4de', 'salmon': '#e88f6c',
'ok': '#a7d68c', 'gold': '#e6b65a', 'band_text': '#f3ecdc', 'status': '#b9a988', 'log_text': '#cfc4ab',
'line': '#54442f', 'dot': '#5d4b36', 'filigree': '#7a6443', 'glow': '#e9ebee',
'tri_top': '#d49f36', 'tri_bottom': '#fcbf44',
}
def rgb(hexcolor):
h = hexcolor.lstrip('#')
return np.array([int(h[i:i + 2], 16) / 255.0 for i in (0, 2, 4)], dtype=np.float32)
# ---------------------------------------------------------------- PNG and Tk
def png_bytes(img):
"""PNG of an HxWx4 float array in 0..1 (straight alpha) or uint8 array."""
a = img if img.dtype == np.uint8 else (np.clip(img, 0, 1) * 255 + 0.5).astype(np.uint8)
h, w = a.shape[:2]
raw = b''.join(b'\0' + a[y].tobytes() for y in range(h))
def chunk(tag, data):
return struct.pack('>I', len(data)) + tag + data + struct.pack('>I', zlib.crc32(tag + data) & 0xFFFFFFFF)
return (b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 6, 0, 0, 0))
+ chunk(b'IDAT', zlib.compress(raw, 3)) + chunk(b'IEND', b''))
def photo(master, img):
import tkinter as tk
return tk.PhotoImage(master=master, data=base64.b64encode(png_bytes(img)).decode('ascii'), format='png')
def ico_bytes(images):
"""A Windows .ico holding PNG-compressed images (one HxWx4 array per size, square)."""
pngs = [png_bytes(im) for im in images]
head = struct.pack('<HHH', 0, 1, len(pngs))
offset = 6 + 16 * len(pngs)
dirs, body = b'', b''
for im, data in zip(images, pngs):
n = im.shape[0]
dirs += struct.pack('<BBBBHHII', n % 256, n % 256, 0, 0, 1, 32, len(data), offset + len(body))
body += data
return head + dirs + body
# ---------------------------------------------------------------- drawing primitives
def new(h, w, color=None, alpha=0.0):
img = np.zeros((h, w, 4), dtype=np.float32)
if color is not None:
img[..., :3] = rgb(color)
img[..., 3] = alpha
return img
def over(dst, src, x=0, y=0):
"""Composite src over dst at (x, y), straight alpha, clipped to dst."""
h, w = src.shape[:2]
H, W = dst.shape[:2]
x0, y0, x1, y1 = max(x, 0), max(y, 0), min(x + w, W), min(y + h, H)
if x0 >= x1 or y0 >= y1:
return dst
s = src[y0 - y:y1 - y, x0 - x:x1 - x]
d = dst[y0:y1, x0:x1]
sa, da = s[..., 3:4], d[..., 3:4]
oa = sa + da * (1 - sa)
oc = np.where(oa > 0, (s[..., :3] * sa + d[..., :3] * da * (1 - sa)) / np.maximum(oa, 1e-6), 0)
d[..., :3], d[..., 3:4] = oc, oa
return dst
def paint(img, mask, color, alpha=1.0):
"""Paint a colour through a coverage mask (HxW, 0..1) onto img."""
layer = new(*mask.shape, color)
layer[..., 3] = np.clip(mask, 0, 1) * alpha
return over(img, layer)
def vgradient(h, w, stops):
"""HxWx3 colours down the height through (position 0..1, colour) stops."""
pos = np.array([p for p, _ in stops], dtype=np.float32)
cols = np.stack([rgb(c) for _, c in stops])
t = (np.arange(h, dtype=np.float32) + 0.5) / max(h, 1)
col = np.stack([np.interp(t, pos, cols[:, k]) for k in range(3)], axis=1)
return np.broadcast_to(col[:, None, :], (h, w, 3)).copy()
def hgradient_alpha(h, w, stops):
"""HxW alphas across the width through (position, alpha) stops."""
pos = np.array([p for p, _ in stops], dtype=np.float32)
al = np.array([a for _, a in stops], dtype=np.float32)
t = (np.arange(w, dtype=np.float32) + 0.5) / max(w, 1)
return np.broadcast_to(np.interp(t, pos, al)[None, :], (h, w)).copy()
def hgradient_rgba(h, w, stops):
"""An RGBA layer whose colour and alpha run across the width through (position, colour, alpha) stops."""
pos = np.array([p for p, _, _ in stops], dtype=np.float32)
cols = np.stack([rgb(c) for _, c, _ in stops])
al = np.array([a for _, _, a in stops], dtype=np.float32)
t = (np.arange(w, dtype=np.float32) + 0.5) / max(w, 1)
layer = new(h, w)
for k in range(3):
layer[..., k] = np.interp(t, pos, cols[:, k])[None, :]
layer[..., 3] = np.interp(t, pos, al)[None, :]
return layer
def value_noise(h, w, period_x, period_y=None, seed=0):
"""Smooth noise in 0..1 with features about period pixels apart."""
period_y = period_y or period_x
rng = np.random.default_rng(seed)
gh, gw = int(h / period_y) + 3, int(w / period_x) + 3
g = rng.random((gh, gw), dtype=np.float32)
ys = np.arange(h, dtype=np.float32) / period_y
xs = np.arange(w, dtype=np.float32) / period_x
y0, x0 = ys.astype(int), xs.astype(int)
fy, fx = ys - y0, xs - x0
fy, fx = fy * fy * (3 - 2 * fy), fx * fx * (3 - 2 * fx)
a, b = g[y0][:, x0], g[y0][:, x0 + 1]
c, d = g[y0 + 1][:, x0], g[y0 + 1][:, x0 + 1]
top = a + (b - a) * fx[None, :]
bot = c + (d - c) * fx[None, :]
return top + (bot - top) * fy[:, None]
def fractal(h, w, period_x, period_y=None, octaves=3, seed=0, contrast=0.4):
"""Fractal noise around mid grey, about contrast wide (the range of an SVG feTurbulence turned grey)."""
return 0.5 + (_fractal01(h, w, period_x, period_y, octaves, seed) - 0.5) * contrast
def _fractal01(h, w, period_x, period_y=None, octaves=3, seed=0):
period_y = period_y or period_x
out = np.zeros((h, w), dtype=np.float32)
amp, total = 1.0, 0.0
for o in range(octaves):
out += amp * value_noise(h, w, max(period_x / 2 ** o, 1.5), max(period_y / 2 ** o, 1.5), seed + o * 101)
total += amp
amp *= 0.5
out /= total
lo, hi = np.percentile(out, 1), np.percentile(out, 99)
return np.clip((out - lo) / max(hi - lo, 1e-6), 0, 1)
def overlay(rgb_arr, n, opacity):
"""CSS mix-blend-mode overlay of a grey noise n (HxW) over colours, at opacity."""
n = n[..., None]
blended = np.where(rgb_arr < 0.5, 2 * rgb_arr * n, 1 - 2 * (1 - rgb_arr) * (1 - n))
return rgb_arr + (blended - rgb_arr) * opacity
def box_blur(a, r):
"""Three box blurs of radius r over a 2-D array (close to a gaussian)."""
r = int(round(r))
if r < 1:
return a
out = a.astype(np.float32)
for _ in range(3):
for axis in (0, 1):
pad = [(0, 0), (0, 0)]
pad[axis] = (r + 1, r)
p = np.pad(out, pad, mode='edge')
c = np.cumsum(p, axis=axis, dtype=np.float64)
if axis == 0:
out = ((c[2 * r + 1:] - c[:-2 * r - 1]) / (2 * r + 1)).astype(np.float32)
else:
out = ((c[:, 2 * r + 1:] - c[:, :-2 * r - 1]) / (2 * r + 1)).astype(np.float32)
return out
def rect_mask(h, w, x0, y0, x1, y1):
"""Coverage of the rectangle [x0, x1) x [y0, y1) with fractional edges."""
xs = np.arange(w, dtype=np.float32)
ys = np.arange(h, dtype=np.float32)
cx = np.clip(np.minimum(xs + 1, x1) - np.maximum(xs, x0), 0, 1)
cy = np.clip(np.minimum(ys + 1, y1) - np.maximum(ys, y0), 0, 1)
return cy[:, None] * cx[None, :]
def ring_mask(h, w, x0, y0, x1, y1, width):
return np.clip(rect_mask(h, w, x0, y0, x1, y1) - rect_mask(h, w, x0 + width, y0 + width, x1 - width, y1 - width),
0, 1)
def polygon_mask(h, w, pts, ss=4):
"""Antialiased coverage of a convex polygon (pixel coordinates), supersampled ss x ss."""
ys = (np.arange(h * ss, dtype=np.float32) + 0.5) / ss
xs = (np.arange(w * ss, dtype=np.float32) + 0.5) / ss
X, Y = np.meshgrid(xs, ys)
inside = np.ones_like(X, dtype=bool)
n = len(pts)
area = sum(pts[i][0] * pts[(i + 1) % n][1] - pts[(i + 1) % n][0] * pts[i][1] for i in range(n))
sign = 1 if area > 0 else -1
for i in range(n):
(ax, ay), (bx, by) = pts[i], pts[(i + 1) % n]
inside &= sign * ((bx - ax) * (Y - ay) - (by - ay) * (X - ax)) >= 0
return inside.reshape(h, ss, w, ss).mean(axis=(1, 3)).astype(np.float32)
def stroke_mask(h, w, polylines, width):
"""Antialiased coverage of polylines (lists of (x, y)) stroked with round joins and caps."""
ys = np.arange(h, dtype=np.float32) + 0.5
xs = np.arange(w, dtype=np.float32) + 0.5
X, Y = np.meshgrid(xs, ys)
best = np.full((h, w), 1e9, dtype=np.float32)
for line in polylines:
P = np.asarray(line, dtype=np.float32)
if len(P) == 1:
P = np.vstack([P, P])
for (ax, ay), (bx, by) in zip(P[:-1], P[1:]):
dx, dy = bx - ax, by - ay
ll = dx * dx + dy * dy
if ll < 1e-9:
d = np.hypot(X - ax, Y - ay)
else:
t = np.clip(((X - ax) * dx + (Y - ay) * dy) / ll, 0, 1)
d = np.hypot(X - (ax + t * dx), Y - (ay + t * dy))
np.minimum(best, d, out=best)
return np.clip(width / 2 + 0.5 - best, 0, 1)
def shadow(h, w, mask, color, alpha, blur, dx=0, dy=0):
"""A soft shadow of a mask: blurred, offset, as an RGBA layer of the same size."""
m = box_blur(np.roll(np.roll(mask, int(dy), 0), int(dx), 1), blur / 2)
layer = new(h, w, color)
layer[..., 3] = np.clip(m, 0, 1) * alpha
return layer
# ---------------------------------------------------------------- SVG path data (the filigree)
def svg_polylines(d, scale=1.0, ox=0.0, oy=0.0, steps=10):
"""Points along an SVG path made of M, L, C and Z commands."""
toks = re.findall(r'[MLCZmlcz]|-?\d*\.?\d+', d)
lines, cur, pos, i, cmd = [], [], (0.0, 0.0), 0, None
start = (0.0, 0.0)
def num():
nonlocal i
v = float(toks[i])
i += 1
return v
while i < len(toks):
if toks[i] in 'MLCZmlcz':
cmd = toks[i]
i += 1
if cmd in 'Zz':
cur.append(start)
continue
if cmd in 'Mm':
if cur:
lines.append(cur)
pos = (num(), num())
start = pos
cur = [pos]
cmd = 'L'
elif cmd in 'Ll':
pos = (num(), num())
cur.append(pos)
elif cmd in 'Cc':
p0 = pos
c1, c2, p1 = (num(), num()), (num(), num()), (num(), num())
for k in range(1, steps + 1):
t = k / steps
u = 1 - t
cur.append((u ** 3 * p0[0] + 3 * u * u * t * c1[0] + 3 * u * t * t * c2[0] + t ** 3 * p1[0],
u ** 3 * p0[1] + 3 * u * u * t * c1[1] + 3 * u * t * t * c2[1] + t ** 3 * p1[1]))
pos = p1
if cur:
lines.append(cur)
return [[((x - ox) * scale, (y - oy) * scale) for x, y in line] for line in lines]
# The game's divider ornament, traced from the options menu: two pointed lens ends, S-waves into spiral hooks and a
# diamond knot under the middle, in a 120 x 38 box (centre x 60).
FILIGREE_PATHS = (
'M20 17.4 C20.4 17.3 21.6 16.8 22.2 16.9 C22.8 17 23.5 17.7 23.5 18.2 C23.5 18.7 23.1 19.4 22.4 19.8 C21.6 20.2 '
'20.3 20.4 19 20.4 C17.7 20.4 15.9 20.2 14.5 19.9 C13.1 19.6 10.6 19.4 10.5 18.6 C10.4 17.9 12.6 16.3 14 15.4 '
'C15.4 14.5 17.2 13.6 19 13.2 C20.8 12.8 23.2 12.8 25 13.1 C26.8 13.4 28.1 13.9 29.5 14.8 C30.9 15.7 32.2 17.4 '
'33.5 18.6 C34.8 19.8 36 21.2 37.5 22 C39 22.8 40.8 23.4 42.5 23.6 C44.2 23.8 46 23.5 47.5 23.1 C49 22.7 50.4 '
'21.9 51.2 21.2 C52 20.5 52.5 19.5 52.4 18.8 C52.3 18.1 51.5 17.3 50.6 16.9 C49.7 16.5 48.2 16.2 47 16.3 C45.8 '
'16.4 44.2 17.1 43.6 17.2',
'M100 17.4 C99.6 17.3 98.4 16.8 97.8 16.9 C97.2 17 96.5 17.7 96.5 18.2 C96.5 18.7 96.8 19.4 97.6 19.8 C98.3 20.2 '
'99.7 20.4 101 20.4 C102.3 20.4 104.1 20.2 105.5 19.9 C106.9 19.6 109.4 19.4 109.5 18.6 C109.6 17.9 107.4 16.3 '
'106 15.4 C104.6 14.5 102.8 13.6 101 13.2 C99.2 12.8 96.8 12.8 95 13.1 C93.2 13.4 91.9 13.9 90.5 14.8 C89.1 15.7 '
'87.8 17.4 86.5 18.6 C85.2 19.8 84 21.2 82.5 22 C81 22.8 79.2 23.4 77.5 23.6 C75.8 23.8 74 23.5 72.5 23.1 C71 '
'22.7 69.6 21.9 68.8 21.2 C68 20.5 67.5 19.5 67.6 18.8 C67.7 18.1 68.5 17.3 69.4 16.9 C70.3 16.5 71.8 16.2 73 '
'16.3 C74.2 16.4 75.8 17.1 76.4 17.2',
'M40.6 17.6 C40.7 17 40.7 14.9 41.2 14 C41.7 13.1 42.6 12.4 43.8 12 C45 11.6 46.7 11.4 48.2 11.5 C49.7 11.6 51.3 '
'11.8 52.6 12.3 C53.9 12.8 55 13.6 56 14.6 C57 15.6 58.2 17.6 58.6 18.2 L64.6 26 L60 31.6',
'M79.4 17.6 C79.3 17 79.3 14.9 78.8 14 C78.3 13.1 77.4 12.4 76.2 12 C75 11.6 73.3 11.4 71.8 11.5 C70.3 11.6 68.7 '
'11.8 67.4 12.3 C66.1 12.8 65 13.6 64 14.6 C63 15.6 61.8 17.6 61.4 18.2 L55.4 26 L60 31.6',
)
# ---------------------------------------------------------------- the window's pieces
def backdrop(W, H, s):
"""The whole window behind the panels: a dim misty field under a see-through dark green, darker at the sides."""
img = new(H, W, PALETTE['window'], 1.0)
m = int(30 * s)
scene = new(H, W - 2 * m)
scene[..., :3] = vgradient(H, W - 2 * m, [(0, '#394244'), (0.36, '#414b4c'), (0.60, '#474234'), (1, '#3e3523')])
scene[..., 3] = 1
over(img, scene, m, 0)
band = np.zeros((H, W), dtype=np.float32)
y0, y1 = int(250 * s), int(330 * s)
t = (np.arange(y1 - y0) + 0.5) / max(y1 - y0, 1)
band[y0:y1, m:W - m] = (np.interp(t, [0, 0.55, 1], [0, 0.85, 0]))[:, None]
paint(img, box_blur(band, 10 * s), '#262a22')
over(img, hgradient_rgba(H, W, [(0, '#05180d', 0.96), (0.14, '#092615', 0.82), (0.5, '#0c2e1b', 0.72),
(0.86, '#092615', 0.82), (1, '#05180d', 0.96)]))
inner = rect_mask(H, W, 0, 0, W, H)
vig = 1 - box_blur(np.pad(inner, int(45 * s), constant_values=0), 45 * s)[int(45 * s):-int(45 * s),
int(45 * s):-int(45 * s)]
paint(img, np.clip(vig * 1.6, 0, 1), '#020e07', 0.6)
return img
def band(w, h, s):
"""The wooden title band, its two-line bottom edge and the shadow under it; (image, extra height below)."""
pad = int(14 * s)
img = new(h + pad, w)
wood = vgradient(h, w, [(0, '#4c3b26'), (0.30, '#5a4529'), (0.65, '#6b5232'), (1, '#806239')])
wood = overlay(wood, fractal(h, w, 300 * s, 11 * s, 3, seed=9), 0.32)
wood = overlay(wood, fractal(h, w, 33 * s, None, 3, seed=4), 0.2)
shade = rect_mask(h + pad, w, 0, 0, w, h + 4 * s)
over(img, shadow(h + pad, w, shade, '#000000', 0.6, 16 * s, 0, 5 * s))
top = new(h, w)
top[..., :3], top[..., 3] = wood, 1
over(img, top)
paint(img, rect_mask(h + pad, w, 0, h, w, h + 2 * s), '#2c2217')
paint(img, rect_mask(h + pad, w, 0, h + 2 * s, w, h + 4 * s), '#a9874f')
return img
def panel(w, h, s):
"""The left list panel with the shadow on its right."""
sh = int(26 * s)
img = new(h, w + sh)
body = rect_mask(h, w + sh, 0, 0, w, h)
over(img, shadow(h, w + sh, body, '#000000', 0.35, 18 * s, 6 * s, 0))
col = np.broadcast_to(rgb(PALETTE['panel']), (h, w, 3)).copy()
col = overlay(col, fractal(h, w, 33 * s, None, 3, seed=4), 0.18)
layer = new(h, w)
layer[..., :3], layer[..., 3] = col, 1
over(img, layer)
return img
def label_cell(w, h, s, bright=False):
"""A bronze label cell: gradient, mottled texture, 1 px border and the 1 px dark ring around the row."""
r = max(1, int(round(s)))
img = new(h + 2 * r, w + 2 * r, PALETTE['ring'], 1.0)
col = vgradient(h, w, [(0, PALETTE['label_top']), (0.5, PALETTE['label_mid']), (1, PALETTE['label_bottom'])])
col = overlay(col, fractal(h, w, 33 * s, None, 3, seed=(w * 7 + h) % 997), 0.22)
if bright:
col = np.clip(col * 1.08, 0, 1)
cell = new(h, w)
cell[..., :3], cell[..., 3] = col, 1
over(img, cell, r, r)
paint(img, ring_mask(h + 2 * r, w + 2 * r, r, r, w + r, h + r, r), PALETTE['label_border'])
return img
def desc_box(w, h, s):
pad = int(20 * s)
img = new(h + 2 * pad, w + 2 * pad)
body = rect_mask(h + 2 * pad, w + 2 * pad, pad, pad, pad + w, pad + h)
over(img, shadow(h + 2 * pad, w + 2 * pad, body, '#000000', 0.35, 18 * s, 0, 6 * s))
col = np.broadcast_to(rgb(PALETTE['desc']), (h, w, 3)).copy()
col = overlay(col, fractal(h, w, 33 * s, None, 3, seed=11), 0.15)
layer = new(h, w)
layer[..., :3], layer[..., 3] = col, 1
over(img, layer, pad, pad)
paint(img, ring_mask(h + 2 * pad, w + 2 * pad, pad, pad, pad + w, pad + h, max(1, round(s))),
PALETTE['desc_border'])
return img, pad
def glow_frames(w, h, s, n=8):
"""The selection cursor around a w x h box: a 3 px white frame 6 px outside it with a soft blue glow inside and
out, n frames of one breath. Returns (frames, margin) where the box sits at (margin, margin)."""
gap, thick, spread = 6 * s, 3 * s, 16 * s
mg = int(gap + spread)
H, W = h + 2 * mg, w + 2 * mg
ring = ring_mask(H, W, mg - gap, mg - gap, mg + w + gap, mg + h + gap, thick)
soft_out = box_blur(ring, 6 * s)
soft_far = box_blur(ring, 12 * s)
frames = []
for k in range(n):
t = 0.5 - 0.5 * np.cos(2 * np.pi * k / n)
img = new(H, W)
paint(img, np.clip(soft_far * 2.2, 0, 1), '#96b4e6', 0.35 + 0.25 * t)
paint(img, np.clip(soft_out * 2.8, 0, 1), '#afc5e8', 0.7 + 0.3 * t)
paint(img, ring, PALETTE['glow'])
frames.append(img)
return frames, mg
def filigree(w, s):
"""The divider: a thin line with a dot at each end and the ornament in the middle; (image, height)."""
h = int(round(26 * s))
img = new(h, w)
mid = h / 2
orn_w = 78 * s
scale = orn_w / 108.0
ox = (w - orn_w) / 2
dot_r = 1.5 * s
gap = 5 * s
left_end, right_start = ox - gap, ox + orn_w + gap
xs0, xs1 = 4 * s + 2 * dot_r + gap, w - 4 * s - 2 * dot_r - gap
paint(img, rect_mask(h, w, xs0, mid - 0.5 * s, left_end, mid + 0.5 * s), PALETTE['line'])
paint(img, rect_mask(h, w, right_start, mid - 0.5 * s, xs1, mid + 0.5 * s), PALETTE['line'])
dots = stroke_mask(h, w, [[(4 * s + dot_r, mid)], [(w - 4 * s - dot_r, mid)]], 2 * dot_r)
paint(img, dots, PALETTE['dot'])
lines = []
for d in FILIGREE_PATHS:
for line in svg_polylines(d, scale, 6, 8):
lines.append([(x + ox, y + mid - 13 * scale) for x, y in line])
paint(img, stroke_mask(h, w, lines, 2.2 * scale), PALETTE['filigree'])
return img, h
def triforce(w):
"""The Triforce, w pixels wide: three triangles, each shading from top to bottom, on a transparent ground."""
h = int(round(w * 217 / 250))
k = w / 250.0
tris = [((125, 1), (186.2, 107), (63.8, 107)), ((62.2, 109), (123.4, 215), (1, 215)),
((187.8, 109), (249, 215), (126.6, 215))]
img = new(h, w)
top, bottom = rgb(PALETTE['tri_top']), rgb(PALETTE['tri_bottom'])
for tri in tris:
pts = [(x * k, y * k) for x, y in tri]
mask = polygon_mask(h, w, pts)
y0, y1 = min(p[1] for p in pts), max(p[1] for p in pts)
t = np.clip((np.arange(h, dtype=np.float32) + 0.5 - y0) / max(y1 - y0, 1e-6), 0, 1)[:, None, None]
layer = new(h, w)
layer[..., :3] = top + (bottom - top) * t
layer[..., 3] = mask
over(img, layer)
return img
def triforce_ico():
"""The launcher's icon at the sizes Windows asks for."""
imgs = []
for n in (16, 24, 32, 48, 64, 128, 256):
t = triforce(n)
sq = new(n, n)
over(sq, t, 0, (n - t.shape[0]) // 2)
imgs.append(sq)
return ico_bytes(imgs)
def check_mark(size, s, color=None):
img = new(size, size)
k = size / 24.0
paint(img, stroke_mask(size, size, [[(5 * k, 12.5 * k), (9.2 * k, 16.7 * k), (19 * k, 7 * k)]], 3 * k),
color or PALETTE['ok'])
return img
def chevron(w, h, color, left=False):
img = new(h, w)
pts = [(w - 0.5, 0), (0.5, h / 2), (w - 0.5, h)] if left else [(0.5, 0), (w - 0.5, h / 2), (0.5, h)]
paint(img, polygon_mask(h, w, pts), color)
return img
def framed_icon(size, s, kind):
"""The square-framed icon of a big row (play or refresh), like the icon frames of the game's menu rows."""
img = new(size, size)
t = 1.5 * s
paint(img, ring_mask(size, size, 0.5, 0.5, size - 0.5, size - 0.5, t), '#e9dcc0')
c = size / 2
if kind == 'play':
k = size / 32
paint(img, polygon_mask(size, size, [(c - 5 * k, c - 7 * k), (c + 7 * k, c), (c - 5 * k, c + 7 * k)]),
'#efe4ca')
elif kind == 'refresh':
import math
k = size / 32
r = 7.5 * k
arcs = []
for a0, a1 in ((200, 340), (20, 160)):
arcs.append([(c + r * math.cos(math.radians(a)), c + r * math.sin(math.radians(a)))
for a in range(a0, a1 + 1, 6)])
heads = [[(c + r * math.cos(math.radians(340)) - 3.5 * k, c + r * math.sin(math.radians(340)) - 1 * k),
(c + r * math.cos(math.radians(340)), c + r * math.sin(math.radians(340))),
(c + r * math.cos(math.radians(340)) + 0.5 * k, c + r * math.sin(math.radians(340)) - 4 * k)],
[(c + r * math.cos(math.radians(160)) + 3.5 * k, c + r * math.sin(math.radians(160)) + 1 * k),
(c + r * math.cos(math.radians(160)), c + r * math.sin(math.radians(160))),
(c + r * math.cos(math.radians(160)) - 0.5 * k, c + r * math.sin(math.radians(160)) + 4 * k)]]
paint(img, stroke_mask(size, size, arcs + heads, 2.2 * k), '#efe4ca')
return img
def badge(size, s):
"""The orange "!" disc beside Updates when a new version is ready (the "!" itself is text)."""
img = new(size + 4, size + 4)
ring = stroke_mask(size + 4, size + 4, [[((size + 4) / 2, (size + 4) / 2)]], size + 1.5 * s)
over(img, shadow(size + 4, size + 4, ring, '#fbbd45', 0.6, 6 * s))
paint(img, ring, '#3a2a14')
disc = stroke_mask(size + 4, size + 4, [[((size + 4) / 2, (size + 4) / 2)]], size)
layer = new(size + 4, size + 4)
layer[..., :3] = vgradient(size + 4, size + 4, [(0, '#fbbd45'), (1, '#e2942a')])
layer[..., 3] = disc
over(img, layer)
return img