diff --git a/client/index.html b/client/index.html index 4088862..1faa727 100644 --- a/client/index.html +++ b/client/index.html @@ -4,6 +4,8 @@ + + Reading Notes diff --git a/client/public/icons/apple-touch-icon.png b/client/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..ccfa88d Binary files /dev/null and b/client/public/icons/apple-touch-icon.png differ diff --git a/client/public/icons/icon-192.png b/client/public/icons/icon-192.png index c793da3..c2bf817 100644 Binary files a/client/public/icons/icon-192.png and b/client/public/icons/icon-192.png differ diff --git a/client/public/icons/icon-512.png b/client/public/icons/icon-512.png index 0ae297d..d6c2bbb 100644 Binary files a/client/public/icons/icon-512.png and b/client/public/icons/icon-512.png differ diff --git a/client/scripts/gen-icons.py b/client/scripts/gen-icons.py index 6e68080..86d39c0 100644 --- a/client/scripts/gen-icons.py +++ b/client/scripts/gen-icons.py @@ -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") diff --git a/client/vite.config.ts b/client/vite.config.ts index 59d7fc4..657825d 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -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" } ] }