#!/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)