build-container / build (push) Successful in 1m56s
- iOS ignores SVG touch icons: add 120/152/167/180 apple-touch-icons, 192/512 and maskable manifest icons, and 25 splash screens (every iPhone size, iPad portrait+landscape incl. 12.9") - Icons and splash screens are public so iOS can fetch them without a session - Manifest: id, scope, categories, shortcuts; iOS standalone polish (no callout, no overscroll) - Sessions last SESSION_DAYS (default 180) and renew daily while in use - tools/make-icons.py regenerates the artwork Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
77 lines
3.9 KiB
Python
77 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Generates the PWA icons and iOS splash screens into public/icons. Needs Pillow: pip install pillow"""
|
|
import os
|
|
from PIL import Image, ImageDraw
|
|
|
|
OUT = os.path.join(os.path.dirname(__file__), '..', 'public', 'icons')
|
|
os.makedirs(OUT, exist_ok=True)
|
|
SS = 4 # supersampling for smooth edges
|
|
|
|
def glyph(size, fg_scale=0.56, bg=((0x5b, 0x86, 0xff), (0x3a, 0x5c, 0xe6)), rounded=0.0):
|
|
"""The app mark: four rounded tiles on a blue gradient. rounded>0 rounds the outer corners (for splash screens)."""
|
|
S = size * SS
|
|
img = Image.new('RGB', (S, S))
|
|
px = img.load() if False else None
|
|
# vertical gradient
|
|
top, bot = bg
|
|
grad = Image.new('RGB', (1, S))
|
|
for y in range(S):
|
|
t = y / (S - 1)
|
|
grad.putpixel((0, y), tuple(int(top[i] + (bot[i] - top[i]) * t) for i in range(3)))
|
|
img = grad.resize((S, S))
|
|
overlay = Image.new('RGBA', (S, S), (0, 0, 0, 0))
|
|
d = ImageDraw.Draw(overlay)
|
|
g = S * fg_scale # glyph box
|
|
gap = g * 0.10
|
|
tile = (g - gap) / 2
|
|
x0 = (S - g) / 2
|
|
r = tile * 0.24
|
|
for i, (cx, cy, a) in enumerate([(0, 0, 255), (1, 0, 180), (0, 1, 180), (1, 1, 255)]):
|
|
x = x0 + cx * (tile + gap); y = x0 + cy * (tile + gap)
|
|
d.rounded_rectangle([x, y, x + tile, y + tile], radius=r, fill=(255, 255, 255, a))
|
|
img = Image.alpha_composite(img.convert('RGBA'), overlay)
|
|
if rounded:
|
|
mask = Image.new('L', (S, S), 0)
|
|
ImageDraw.Draw(mask).rounded_rectangle([0, 0, S - 1, S - 1], radius=S * rounded, fill=255)
|
|
out = Image.new('RGBA', (S, S), (0, 0, 0, 0)); out.paste(img, (0, 0), mask); img = out
|
|
return img.resize((size, size), Image.LANCZOS)
|
|
|
|
def save(img, name, flatten=True):
|
|
if flatten and img.mode == 'RGBA': # iOS fills transparency with black, so icons must be opaque
|
|
bg = Image.new('RGB', img.size, (0x3a, 0x5c, 0xe6)); bg.paste(img, mask=img.split()[3]); img = bg
|
|
img.save(os.path.join(OUT, name), optimize=True)
|
|
|
|
# ---- Icons
|
|
for n in (180, 167, 152, 120):
|
|
save(glyph(n), f'apple-touch-icon-{n}.png')
|
|
save(glyph(180), 'apple-touch-icon.png') # iOS default lookup
|
|
save(glyph(192), 'icon-192.png'); save(glyph(512), 'icon-512.png')
|
|
save(glyph(512, fg_scale=0.50), 'icon-maskable-512.png') # artwork stays inside the 80% safe zone
|
|
save(glyph(192, fg_scale=0.50), 'icon-maskable-192.png')
|
|
save(glyph(32), 'favicon-32.png')
|
|
|
|
# ---- iOS splash screens: dark background with the app mark. (width, height, css width, css height, pixel ratio)
|
|
BG = (0x0f, 0x11, 0x15)
|
|
PHONES = [(1320, 2868, 440, 956, 3), (1206, 2622, 402, 874, 3), (1290, 2796, 430, 932, 3), (1179, 2556, 393, 852, 3), (1284, 2778, 428, 926, 3),
|
|
(1170, 2532, 390, 844, 3), (1125, 2436, 375, 812, 3), (1242, 2688, 414, 896, 3), (828, 1792, 414, 896, 2), (1242, 2208, 414, 736, 3), (750, 1334, 375, 667, 2)]
|
|
PADS = [(2048, 2732, 1024, 1366, 2), (1668, 2388, 834, 1194, 2), (1668, 2224, 834, 1112, 2), (1640, 2360, 820, 1180, 2), (1620, 2160, 810, 1080, 2),
|
|
(1488, 2266, 744, 1133, 2), (1536, 2048, 768, 1024, 2)]
|
|
links = []
|
|
def splash(w, h, cw, ch, pr, orient):
|
|
img = Image.new('RGB', (w, h), BG)
|
|
m = int(min(w, h) * 0.22)
|
|
mark = glyph(m, rounded=0.225)
|
|
img.paste(mark, ((w - m) // 2, (h - m) // 2), mark)
|
|
name = f'splash-{w}x{h}.png'
|
|
img.save(os.path.join(OUT, name), optimize=True)
|
|
links.append((name, cw, ch, pr, orient))
|
|
for (w, h, cw, ch, pr) in PHONES: splash(w, h, cw, ch, pr, 'portrait')
|
|
for (w, h, cw, ch, pr) in PADS:
|
|
splash(w, h, cw, ch, pr, 'portrait')
|
|
splash(h, w, ch, cw, pr, 'landscape') # the same device on its side
|
|
|
|
with open(os.path.join(OUT, 'splash-links.html'), 'w') as f:
|
|
for name, cw, ch, pr, o in links:
|
|
f.write(f'<link rel="apple-touch-startup-image" href="/icons/{name}" media="(device-width: {cw}px) and (device-height: {ch}px) and (-webkit-device-pixel-ratio: {pr}) and (orientation: {o})">\n')
|
|
print(len(links), 'splash screens')
|