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:
parent
47d9113f3c
commit
39f7eb8804
6 changed files with 101 additions and 13 deletions
|
|
@ -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>
|
||||
|
|
|
|||
BIN
client/public/icons/apple-touch-icon.png
Normal file
BIN
client/public/icons/apple-touch-icon.png
Normal file
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 |
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue