Files
audiobook-player/tools/make_icons.py
T
nmemmertandClaude Sonnet 5.5 429a72f2c3
build / check (push) Successful in 10s
build / image (push) Successful in 31s
New headphones app icon set, hardened streaming and playback, download removal controls
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 14:11:31 -04:00

67 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""Draws the app icons (headphones on an orange gradient). Run from the repo root: python3 tools/make_icons.py"""
from PIL import Image, ImageDraw, ImageFilter
S = 1024
def gradient():
# diagonal orange gradient with a soft highlight, built small and scaled up for smoothness
n = 256
img = Image.new("RGB", (n, n))
px = img.load()
for y in range(n):
for x in range(n):
t = (x + y) / (2 * n)
r = 255 + (214 - 255) * t
g = 142 + (66 - 142) * t
b = 62 + (6 - 62) * t
d = ((x - n * .25) ** 2 + (y - n * .2) ** 2) ** .5 / n # highlight at top-left
k = max(0, .22 - d * .35)
px[x, y] = (min(255, int(r + 255 * k)), min(255, int(g + 255 * k)), min(255, int(b + 255 * k)))
return img.resize((S, S), Image.BICUBIC)
def headphones():
"""White headphones on a transparent 1024 canvas, centered."""
layer = Image.new("RGBA", (S, S), (0, 0, 0, 0))
d = ImageDraw.Draw(layer)
white, pad = (255, 255, 255, 255), (214, 74, 10, 255)
cx, cy, ro, w = 512, 530, 330, 76
d.arc((cx - ro, cy - ro, cx + ro, cy + ro), 180, 360, fill=white, width=w) # headband
for side in (-1, 1):
ex = cx + side * (ro - w / 2) # band end
d.rounded_rectangle((ex - 80, cy - 4, ex + 80, cy + 290), 78, fill=white) # ear cup
ix = ex - side * 8 + (-side) * 0
px0, px1 = (ex + 8, ex + 80) if side == -1 else (ex - 80, ex - 8)
d.rounded_rectangle((px0, cy + 44, px1, cy + 246), 34, fill=pad) # cushion
# content box ~ (130..894, 200..820)
return layer
def compose(scale, shadow=True):
bg = gradient().convert("RGBA")
hp = headphones()
if scale != 1:
size = int(S * scale)
hp = hp.resize((size, size), Image.LANCZOS)
full = Image.new("RGBA", (S, S), (0, 0, 0, 0))
full.paste(hp, ((S - size) // 2, (S - size) // 2 + int(S * (1 - scale) * .05)), hp)
hp = full
if shadow:
sh = Image.new("RGBA", (S, S), (0, 0, 0, 0))
sh.putalpha(hp.getchannel("A").filter(ImageFilter.GaussianBlur(22)).point(lambda a: int(a * .35)))
offset = Image.new("RGBA", (S, S), (90, 25, 0, 0))
offset.putalpha(sh.getchannel("A"))
shifted = Image.new("RGBA", (S, S), (0, 0, 0, 0)); shifted.paste(offset, (0, 18))
bg = Image.alpha_composite(bg, shifted)
return Image.alpha_composite(bg, hp).convert("RGB")
any_icon = compose(1.0) # 'any' and apple-touch: full-bleed, iOS rounds the corners itself
maskable = compose(.70) # content kept inside the 80% safe zone so Android can crop to any shape
for size, name, src in ((512, "icon-512.png", any_icon), (192, "icon-192.png", any_icon), (180, "icon-180.png", any_icon),
(512, "icon-maskable-512.png", maskable), (32, "icon-32.png", any_icon)):
src.resize((size, size), Image.LANCZOS).save(name, optimize=True)
print("wrote", name)