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"
}
]
}