#!/usr/bin/env python3 """Draw the home-screen icon, with no image library to install. python3 tools/make_icons.py Writes static/icon-180.png (what iOS uses for the home screen) and static/icon-512.png (Android / the web manifest). Re-run it if you want different colours -- they are the two constants below. """ import math import struct import zlib from pathlib import Path TOP = (0x1D, 0x27, 0x3C) # background gradient, top BOTTOM = (0x0A, 0x0D, 0x14) # background gradient, bottom GLYPH = (0xEE, 0xF2, 0xF9) # the wave itself STATIC = Path(__file__).resolve().parent.parent / "static" SAMPLES = 3 # supersampling per axis, for smooth edges def coverage(x: float, y: float, size: float) -> float: """How much of the glyph covers this point: a dot plus three arcs opening to the right, i.e. the usual 'sound coming out' mark.""" cx, cy = 0.33 * size, 0.5 * size dx, dy = x - cx, y - cy distance = math.hypot(dx, dy) if distance <= 0.075 * size: return 1.0 angle = abs(math.degrees(math.atan2(dy, dx))) if angle > 50: return 0.0 half = 0.024 * size for radius in (0.17, 0.27, 0.37): if abs(distance - radius * size) <= half: return 1.0 return 0.0 def render(size: int) -> bytes: rows = bytearray() step = 1.0 / SAMPLES for py in range(size): rows.append(0) # PNG filter type 0 for this scanline mix = py / max(1, size - 1) background = tuple( round(TOP[i] + (BOTTOM[i] - TOP[i]) * mix) for i in range(3) ) for px in range(size): hits = 0 for sy in range(SAMPLES): for sx in range(SAMPLES): if coverage(px + (sx + 0.5) * step, py + (sy + 0.5) * step, size): hits += 1 alpha = hits / (SAMPLES * SAMPLES) if alpha == 0: rows.extend(background) else: rows.extend( round(background[i] + (GLYPH[i] - background[i]) * alpha) for i in range(3) ) return bytes(rows) def write_png(path: Path, size: int, raw: bytes): def chunk(kind: bytes, data: bytes) -> bytes: return (struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF)) header = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0) # 8-bit truecolour path.write_bytes( b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", header) + chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b"") ) if __name__ == "__main__": for size in (180, 512): target = STATIC / f"icon-{size}.png" write_png(target, size, render(size)) print(f"wrote {target} ({target.stat().st_size} bytes)")