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>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user