#!/usr/bin/env python3 """Generate cmd/pad/appicon.png, the Pad app icon. Design: a soft, light cream pillow on a dark ink field, with "P_" (the underscore standing in for the text cursor) embroidered on it in slate thread. gogio picks appicon.png up automatically from the main package (cmd/pad) and builds the Android mipmap/adaptive-icon set and the iOS icon set from it. Usage: scripts/make_icon.py Requires: python3 with PIL. """ import pathlib from PIL import Image, ImageDraw, ImageFilter, ImageFont SIZE = 1024 INK = (32, 38, 50) # background PILLOW = (243, 231, 211) # base fabric PILLOW_LIT = (252, 246, 234) PILLOW_RIM = (206, 187, 158) THREAD = (92, 104, 134) # embroidered slate thread THREAD_DARK = (58, 67, 92) FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf" def rrect_mask(w, h, r): m = Image.new("L", (w, h), 0) ImageDraw.Draw(m).rounded_rectangle([0, 0, w - 1, h - 1], radius=r, fill=255) return m def main() -> None: img = Image.new("RGB", (SIZE, SIZE), INK) # --- pillow geometry (kept inside the adaptive-icon safe zone) --- pw, ph, pr = 672, 612, 195 # pillow size + corner radius px0, py0 = (SIZE - pw) // 2, (SIZE - ph) // 2 + 6 # --- soft drop shadow (slightly below, so light comes from above) --- shadow = Image.new("L", (SIZE, SIZE), 0) d = ImageDraw.Draw(shadow) d.rounded_rectangle([px0 + 16, py0 + 30, px0 + pw - 16, py0 + ph + 36], radius=pr + 10, fill=120) shadow = shadow.filter(ImageFilter.GaussianBlur(42)) img = Image.composite(Image.new("RGB", (SIZE, SIZE), (18, 21, 29)), img, shadow) # --- pillow body: plump radial shading --- base = Image.new("RGB", (SIZE, SIZE), INK) base.paste(Image.new("RGB", (pw, ph), PILLOW), (px0, py0)) sharp = rrect_mask(pw, ph, pr) inner = sharp.filter(ImageFilter.GaussianBlur(pr * 0.8)) lit = Image.new("RGB", (pw, ph), PILLOW_LIT) rim = Image.new("RGB", (pw, ph), PILLOW_RIM) base.paste(lit, (px0, py0), inner) # bright center rim_mask = Image.composite(Image.new("L", (pw, ph), 0), sharp, inner) # edge only rim_mask = rim_mask.point(lambda v: min(255, v * 2)) base.paste(rim, (px0, py0), rim_mask) # shaded edge # top sheen: an out-of-focus light patch sheen = Image.new("L", (SIZE, SIZE), 0) d = ImageDraw.Draw(sheen) d.ellipse([px0 + 110, py0 + 30, px0 + pw - 110, py0 + ph // 2 + 40], fill=80) sheen = sheen.filter(ImageFilter.GaussianBlur(75)) base = Image.composite(Image.new("RGB", (SIZE, SIZE), PILLOW_LIT), base, sheen) # corner dimples: soft darkening where a puffed pillow pinches for cx, cy in [ (px0 + pr - 20, py0 + pr - 20), (px0 + pw - pr + 20, py0 + pr - 20), (px0 + pr - 20, py0 + ph - pr + 20), (px0 + pw - pr + 20, py0 + ph - pr + 20), ]: dip = Image.new("L", (SIZE, SIZE), 0) ImageDraw.Draw(dip).ellipse([cx - 70, cy - 70, cx + 70, cy + 70], fill=55) dip = dip.filter(ImageFilter.GaussianBlur(30)) base = Image.composite( Image.blend(base, Image.new("RGB", (SIZE, SIZE), (214, 196, 168)), 0.5), base, dip) # --- dashed seam line, like a quilted pillow --- # Sample the rounded-rect path finely, then draw dash segments along it. m = 44 # margin from the pillow edge r = pr - m # corner radius of the seam import math as _m # Corners clockwise: center, angle start -> end (screen coords, y down). # Each arc is followed by the straight side to the next arc's start. arcs = [ (m + r, m + r, _m.pi, 1.5 * _m.pi), # top-left (pw - m - r, m + r, 1.5 * _m.pi, 2 * _m.pi), # top-right (pw - m - r, ph - m - r, 0, 0.5 * _m.pi), # bottom-right (m + r, ph - m - r, 0.5 * _m.pi, _m.pi), # bottom-left ] samples = [] for i, (cx, cy, a0, a1) in enumerate(arcs): nx, ny, na0, _ = arcs[(i + 1) % 4] n = max(2, int(abs(a1 - a0) * r // 3)) for k in range(n + 1): a = a0 + (a1 - a0) * k / n samples.append((cx + r * _m.cos(a), cy + r * _m.sin(a))) # straight side from this arc's end to the next arc's start end = (nx + r * _m.cos(na0), ny + r * _m.sin(na0)) last = samples[-1] n = max(2, int(_m.hypot(end[0] - last[0], end[1] - last[1]) // 3)) for k in range(1, n + 1): samples.append((last[0] + (end[0] - last[0]) * k / n, last[1] + (end[1] - last[1]) * k / n)) # cumulative arc length cum = [0.0] for i in range(1, len(samples)): cum.append(cum[-1] + _m.hypot(samples[i][0] - samples[i - 1][0], samples[i][1] - samples[i - 1][1])) total = cum[-1] dash, gap = 26.0, 18.0 seam_ring = Image.new("L", (pw, ph), 0) d = ImageDraw.Draw(seam_ring) s0 = 0.0 while s0 < total: s1 = min(s0 + dash, total) pts = [(x, y) for (x, y), c in zip(samples, cum) if s0 <= c <= s1] if len(pts) > 1: d.line(pts, fill=255, width=9, joint="curve") s0 += dash + gap seam_ring = seam_ring.filter(ImageFilter.GaussianBlur(1.2)) seam = seam_ring seam_full = Image.new("L", (SIZE, SIZE), 0) seam_full.paste(seam, (px0, py0)) seam_col = Image.new("RGB", (SIZE, SIZE), (203, 183, 154)) base = Image.composite(seam_col, base, seam_full) # composite pillow onto background via its sharp mask full_sharp = Image.new("L", (SIZE, SIZE), 0) full_sharp.paste(sharp, (px0, py0)) img.paste(base, (0, 0), full_sharp) # --- embroidered "P_" --- # Center the CAP of the P on the pillow (optical center); the # underscore hangs below the baseline like a cursor on the next line. font = ImageFont.truetype(FONT, 300) # Drawing "P" at (x, y) puts its cap top at y + pb[1]; center the cap # on the pillow, keep the whole "P_" string on the same baseline. probe = ImageDraw.Draw(img) pb = probe.textbbox((0, 0), "P", font=font) cb_h = pb[3] - pb[1] # cap height ty = SIZE // 2 - cb_h // 2 - pb[1] tb = probe.textbbox((0, 0), "P_", font=font) tw, th = tb[2] - tb[0], tb[3] - tb[1] tx = (SIZE - tw) // 2 - tb[0] # stitch texture: fine diagonal thread lines, tiled tile = 6 pat = Image.new("L", (tile, tile), 160) ImageDraw.Draw(pat).line([(0, tile - 1), (tile - 1, 0)], fill=255, width=1) pat = pat.filter(ImageFilter.GaussianBlur(0.5)) mw, mh = tw + 40, tb[3] + 80 stitch = pat.resize((mw, mh), Image.NEAREST) # text mask in local coords # local (0,0) == the absolute drawing origin (tx, ty); paste happens at # (tx-20, ty-20), so text goes at (20 - tb[0], 20). mask = Image.new("L", (mw, mh), 0) ImageDraw.Draw(mask).text((20 - tb[0], 20), "P_", font=font, fill=255) # drop shadow of the stitches on the fabric (thread sits on top of it) sh = mask.filter(ImageFilter.GaussianBlur(5)) sh_full = Image.new("L", (SIZE, SIZE), 0) sh_full.paste(sh, (tx - 20, ty - 20 + 7)) img = Image.composite(Image.new("RGB", (SIZE, SIZE), (182, 164, 140)), img, sh_full) # thread color modulated by the stitch pattern (subtle per-stitch depth) stitch_full = Image.new("L", (SIZE, SIZE), 0) stitch_full.paste(stitch, (tx - 20, ty - 20)) mod = Image.composite(Image.new("RGB", (SIZE, SIZE), THREAD_DARK), Image.new("RGB", (SIZE, SIZE), THREAD), stitch_full) mask_full = Image.new("L", (SIZE, SIZE), 0) mask_full.paste(mask, (tx - 20, ty - 20)) img = Image.composite(mod, img, mask_full) out = pathlib.Path(__file__).resolve().parent.parent / "cmd" / "pad" / "appicon.png" img.save(out) print(f"wrote {out} ({SIZE}x{SIZE})") if __name__ == "__main__": main()