The generated icons were solid brand-blue squares with no glyph. gen-icons.py now procedurally draws a page/notes icon (folded corner, text lines) with anti-aliasing, kept within the maskable-icon safe zone so Android's adaptive-icon masks don't clip it. Also adds an apple-touch-icon and wires favicon/apple-touch-icon links into index.html, and marks manifest icons purpose "any maskable". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C3DGg1pvmvf81zXs4pk1J7
116 lines
3.7 KiB
Python
116 lines
3.7 KiB
Python
"""Generates the PWA app icons as flat, dependency-free PNGs.
|
|
|
|
Draws a simple "page with folded corner + text lines" glyph on a solid
|
|
brand-blue background, using supersampled coverage for anti-aliasing.
|
|
No image libraries required (stdlib struct/zlib only) so this can run
|
|
anywhere Python 3 runs.
|
|
"""
|
|
import struct
|
|
import zlib
|
|
import os
|
|
|
|
BRAND_BLUE = (31, 111, 235)
|
|
PAGE_WHITE = (255, 255, 255)
|
|
FOLD_SHADE = (198, 214, 245)
|
|
LINE_BLUE = (99, 150, 235)
|
|
|
|
SUPERSAMPLE = 4
|
|
|
|
|
|
def mix(c1, c2, t):
|
|
return tuple(round(a + (b - a) * t) for a, b in zip(c1, c2))
|
|
|
|
|
|
def rounded_rect_contains(x, y, left, top, right, bottom, radius):
|
|
if left <= x <= right and top <= y <= bottom:
|
|
cx = min(max(x, left + radius), right - radius)
|
|
cy = min(max(y, top + radius), bottom - radius)
|
|
return (x - cx) ** 2 + (y - cy) ** 2 <= radius * radius
|
|
return False
|
|
|
|
|
|
def line_contains(x, y, cx, cy, half_w, half_h):
|
|
return abs(x - cx) <= half_w and abs(y - cy) <= half_h
|
|
|
|
|
|
def pixel_color(x, y, size):
|
|
# Kept within the maskable-icon "safe zone" (content within a circle of
|
|
# radius 0.4 * size centered on the canvas) so Android's adaptive-icon
|
|
# masks (circle, squircle, rounded square, ...) never clip the glyph.
|
|
left = size * 0.25
|
|
right = size * 0.75
|
|
top = size * 0.20
|
|
bottom = size * 0.80
|
|
radius = size * 0.045
|
|
fold = size * 0.14
|
|
|
|
in_page = rounded_rect_contains(x, y, left, top, right, bottom, radius)
|
|
# Cut the top-right corner off diagonally to form a folded-page look.
|
|
if in_page and (x - (right - fold)) > 0 and (top - y) < 0 and (x - (right - fold)) + (y - top) > fold:
|
|
in_page = False
|
|
|
|
if in_page:
|
|
# Shadow triangle for the folded flap, just inside the cut edge.
|
|
dx = x - (right - fold)
|
|
dy = y - top
|
|
if dx > 0 and dy >= 0 and dx + dy <= fold and dx + dy >= fold * 0.55:
|
|
return FOLD_SHADE
|
|
|
|
page_w = right - left
|
|
line_h = size * 0.028
|
|
line_specs = [
|
|
(0.46, 0.70),
|
|
(0.60, 0.70),
|
|
(0.74, 0.46),
|
|
]
|
|
for rel_y, rel_w in line_specs:
|
|
cy = top + (bottom - top) * rel_y
|
|
half_h = line_h / 2
|
|
half_w = page_w * rel_w / 2
|
|
cx = left + page_w / 2
|
|
if line_contains(x, y, cx, cy, half_w, half_h):
|
|
return LINE_BLUE
|
|
return PAGE_WHITE
|
|
|
|
return BRAND_BLUE
|
|
|
|
|
|
def make_png(path, size):
|
|
raw = bytearray()
|
|
step = 1.0 / SUPERSAMPLE
|
|
for y in range(size):
|
|
raw.append(0) # filter type 0 for each scanline
|
|
for x in range(size):
|
|
r_acc = g_acc = b_acc = 0
|
|
for sy in range(SUPERSAMPLE):
|
|
for sx in range(SUPERSAMPLE):
|
|
sample_x = x + (sx + 0.5) * step
|
|
sample_y = y + (sy + 0.5) * step
|
|
r, g, b = pixel_color(sample_x, sample_y, size)
|
|
r_acc += r
|
|
g_acc += g
|
|
b_acc += b
|
|
n = SUPERSAMPLE * SUPERSAMPLE
|
|
raw += bytes([round(r_acc / n), round(g_acc / n), round(b_acc / n)])
|
|
|
|
compressed = zlib.compress(bytes(raw), 9)
|
|
|
|
def chunk(tag, data):
|
|
return struct.pack(">I", len(data)) + tag + data + struct.pack(
|
|
">I", zlib.crc32(tag + data) & 0xFFFFFFFF
|
|
)
|
|
|
|
sig = b"\x89PNG\r\n\x1a\n"
|
|
ihdr = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0)
|
|
png = sig + chunk(b"IHDR", ihdr) + chunk(b"IDAT", compressed) + chunk(b"IEND", b"")
|
|
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
with open(path, "wb") as f:
|
|
f.write(png)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
make_png("public/icons/icon-192.png", 192)
|
|
make_png("public/icons/icon-512.png", 512)
|
|
make_png("public/icons/apple-touch-icon.png", 180)
|
|
print("Icons generated")
|