Files
whattodo/tools/make_icons.py
T
goyban c5b049edba Initial WhatToDo app
Self-hosted personal task list: FastAPI + SQLite in Docker, mobile-friendly
PWA frontend, deadlines, priorities, categories, tags, notes, checklists,
recurring tasks, single-password login, and JSON export/import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:43:56 +02:00

66 lines
2.6 KiB
Python

"""Generate the PNG app icons (pure Python, no dependencies).
Run inside Docker from the repo root:
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/w -w /w python:3.13-slim python tools/make_icons.py
"""
import math
import struct
import zlib
from pathlib import Path
OUT = Path(__file__).resolve().parent.parent / "app" / "static" / "icons"
BG = (0x4F, 0x46, 0xE5)
FG = (0xFF, 0xFF, 0xFF)
# Check mark in unit coordinates, matching icon.svg.
POINTS = [(0.28, 0.53), (0.44, 0.68), (0.73, 0.36)]
STROKE = 0.09
def seg_dist(px, py, ax, ay, bx, by):
dx, dy = bx - ax, by - ay
t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)))
return math.hypot(px - (ax + t * dx), py - (ay + t * dy))
def rounded_rect_alpha(x, y, size, radius):
# Signed-distance antialiasing for a rounded square covering the whole image.
cx = min(max(x, radius), size - radius)
cy = min(max(y, radius), size - radius)
d = math.hypot(x - cx, y - cy) - radius
return max(0.0, min(1.0, 0.5 - d))
def render(size, rounded, scale=1.0):
radius = size * 0.22 if rounded else 0
pts = [((0.5 + (x - 0.5) * scale) * size, (0.5 + (y - 0.5) * scale) * size) for x, y in POINTS]
half = STROKE * scale * size / 2
rows = []
for j in range(size):
row = bytearray([0])
for i in range(size):
x, y = i + 0.5, j + 0.5
a_bg = rounded_rect_alpha(x, y, size, radius) if rounded else 1.0
d = min(seg_dist(x, y, *pts[0], *pts[1]), seg_dist(x, y, *pts[1], *pts[2]))
a_fg = max(0.0, min(1.0, half - d + 0.5))
rgb = [round(b * (1 - a_fg) + f * a_fg) for b, f in zip(BG, FG)]
row += bytes(rgb) + bytes([round(255 * a_bg)])
rows.append(bytes(row))
return png(size, size, b"".join(rows))
def png(w, h, raw):
def chunk(tag, data):
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data))
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)
return b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b"")
if __name__ == "__main__":
OUT.mkdir(parents=True, exist_ok=True)
(OUT / "icon-192.png").write_bytes(render(192, rounded=True))
(OUT / "icon-512.png").write_bytes(render(512, rounded=True))
# Maskable/Apple icons: full-bleed background, check kept inside the safe zone.
(OUT / "icon-maskable-512.png").write_bytes(render(512, rounded=False, scale=0.8))
(OUT / "icon-180.png").write_bytes(render(180, rounded=False, scale=0.9))
print("icons written to", OUT)