Add a real PWA icon (notes glyph) instead of a flat color placeholder

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
This commit is contained in:
Claudio Schaad 2026-08-27 17:54:17 +02:00
parent 47d9113f3c
commit 39f7eb8804
6 changed files with 101 additions and 13 deletions

View file

@ -4,6 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#1f6feb" />
<link rel="icon" type="image/png" href="/icons/icon-192.png" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
<title>Reading Notes</title>
</head>
<body>

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -1,15 +1,98 @@
"""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
def make_png(path, size, rgb):
width = height = size
r, g, b = rgb
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()
for _ in range(height):
step = 1.0 / SUPERSAMPLE
for y in range(size):
raw.append(0) # filter type 0 for each scanline
for _ in range(width):
raw += bytes([r, g, b])
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):
@ -18,15 +101,16 @@ def make_png(path, size, rgb):
)
sig = b"\x89PNG\r\n\x1a\n"
ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
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__":
color = (31, 111, 235) # brand blue
make_png("public/icons/icon-192.png", 192, color)
make_png("public/icons/icon-512.png", 512, color)
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")

View file

@ -7,7 +7,7 @@ export default defineConfig({
react(),
VitePWA({
registerType: "autoUpdate",
includeAssets: ["icons/icon-192.png", "icons/icon-512.png"],
includeAssets: ["icons/icon-192.png", "icons/icon-512.png", "icons/apple-touch-icon.png"],
manifest: {
name: "Reading Notes",
short_name: "Notes",
@ -20,12 +20,14 @@ export default defineConfig({
{
src: "icons/icon-192.png",
sizes: "192x192",
type: "image/png"
type: "image/png",
purpose: "any maskable"
},
{
src: "icons/icon-512.png",
sizes: "512x512",
type: "image/png"
type: "image/png",
purpose: "any maskable"
}
]
}