diff --git a/README.md b/README.md
index 515f4de..f4e8904 100644
--- a/README.md
+++ b/README.md
@@ -186,7 +186,7 @@ GET /avr/raw?cmd=SSFUN ? # any Telnet command, every line back
```bash
python3 app.py --demo # fake speakers, real interface
python3 -m unittest discover -s tests -t . # runs against a fake HEOS network
-python3 tools/make_icons.py # redraw the home-screen icon
+python3 tools/make_icons.py # re-render the icons from static/logo.svg
```
## Layout
diff --git a/static/icon-180.png b/static/icon-180.png
index b374d6d..38fe83a 100644
Binary files a/static/icon-180.png and b/static/icon-180.png differ
diff --git a/static/icon-512.png b/static/icon-512.png
index c3ee4a2..b1c66c2 100644
Binary files a/static/icon-512.png and b/static/icon-512.png differ
diff --git a/static/logo.svg b/static/logo.svg
new file mode 100644
index 0000000..072f46b
--- /dev/null
+++ b/static/logo.svg
@@ -0,0 +1,8 @@
+
diff --git a/static/panel.css b/static/panel.css
index ff51df5..db568dc 100644
--- a/static/panel.css
+++ b/static/panel.css
@@ -59,7 +59,8 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
/* --- header ---------------------------------------------------------- */
.top { display: flex; align-items: center; justify-content: space-between; padding: 6px 4px 0; }
-.top h1 { margin: 0; font-size: 22px; font-weight: 650; letter-spacing: .02em; }
+.top h1 { margin: 0; line-height: 0; }
+.top .logo { height: 34px; width: auto; display: block; }
.icon-button {
width: 44px; height: 44px; border-radius: 50%;
diff --git a/templates/index.html b/templates/index.html
index 210ac8b..3d47875 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -21,7 +21,7 @@
- {{ app_name }}
+  }})
diff --git a/tools/make_icons.py b/tools/make_icons.py
index 0d40a8a..a29f4c6 100644
--- a/tools/make_icons.py
+++ b/tools/make_icons.py
@@ -1,88 +1,66 @@
#!/usr/bin/env python3
-"""Draw the home-screen icon, with no image library to install.
+"""Render static/logo.svg into the home-screen icons.
python3 tools/make_icons.py
-Writes static/icon-180.png (what iOS uses for the home screen) and
-static/icon-512.png (Android / the web manifest). Re-run it if you want
-different colours -- they are the two constants below.
+Writes static/icon-180.png (what iOS uses on the home screen) and
+static/icon-512.png (Android / the web manifest): the white mark on a
+transparent background, which iOS lays over black on the home screen.
+
+The PNGs are committed, so this only needs running if the logo changes.
+It rasterises with headless Chromium, which is a heavy thing to install
+for one job -- if you have librsvg to hand, this does the same:
+
+ rsvg-convert -w 512 -h 512 static/logo.svg -o static/icon-512.png
+
+Otherwise: pip install playwright && playwright install chromium
"""
-import math
-import struct
-import zlib
+import sys
from pathlib import Path
-TOP = (0x1D, 0x27, 0x3C) # background gradient, top
-BOTTOM = (0x0A, 0x0D, 0x14) # background gradient, bottom
-GLYPH = (0xEE, 0xF2, 0xF9) # the wave itself
-
STATIC = Path(__file__).resolve().parent.parent / "static"
-SAMPLES = 3 # supersampling per axis, for smooth edges
+LOGO = STATIC / "logo.svg"
+SIZES = (180, 512)
+MARGIN = 0.14 # breathing room, so iOS's rounded mask never clips it
+
+PAGE = """
+
+
+"""
-def coverage(x: float, y: float, size: float) -> float:
- """How much of the glyph covers this point: a dot plus three arcs
- opening to the right, i.e. the usual 'sound coming out' mark."""
- cx, cy = 0.33 * size, 0.5 * size
- dx, dy = x - cx, y - cy
- distance = math.hypot(dx, dy)
+def main():
+ try:
+ from playwright.sync_api import sync_playwright
+ except ImportError:
+ sys.exit("needs playwright: pip install playwright && playwright install chromium")
- if distance <= 0.075 * size:
- return 1.0
+ if not LOGO.exists():
+ sys.exit(f"no logo at {LOGO}")
- angle = abs(math.degrees(math.atan2(dy, dx)))
- if angle > 50:
- return 0.0
-
- half = 0.024 * size
- for radius in (0.17, 0.27, 0.37):
- if abs(distance - radius * size) <= half:
- return 1.0
- return 0.0
-
-
-def render(size: int) -> bytes:
- rows = bytearray()
- step = 1.0 / SAMPLES
- for py in range(size):
- rows.append(0) # PNG filter type 0 for this scanline
- mix = py / max(1, size - 1)
- background = tuple(
- round(TOP[i] + (BOTTOM[i] - TOP[i]) * mix) for i in range(3)
- )
- for px in range(size):
- hits = 0
- for sy in range(SAMPLES):
- for sx in range(SAMPLES):
- if coverage(px + (sx + 0.5) * step, py + (sy + 0.5) * step, size):
- hits += 1
- alpha = hits / (SAMPLES * SAMPLES)
- if alpha == 0:
- rows.extend(background)
- else:
- rows.extend(
- round(background[i] + (GLYPH[i] - background[i]) * alpha) for i in range(3)
- )
- return bytes(rows)
-
-
-def write_png(path: Path, size: int, raw: bytes):
- def chunk(kind: bytes, data: bytes) -> bytes:
- return (struct.pack(">I", len(data)) + kind + data
- + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF))
-
- header = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0) # 8-bit truecolour
- path.write_bytes(
- b"\x89PNG\r\n\x1a\n"
- + chunk(b"IHDR", header)
- + chunk(b"IDAT", zlib.compress(raw, 9))
- + chunk(b"IEND", b"")
- )
+ scratch = STATIC / "_icon.html"
+ try:
+ with sync_playwright() as p:
+ browser = p.chromium.launch(args=["--no-sandbox"])
+ for size in SIZES:
+ inner = round(size * (1 - 2 * MARGIN))
+ scratch.write_text(PAGE.format(size=size, inner=inner))
+ page = browser.new_page(viewport={"width": size, "height": size})
+ page.goto(scratch.as_uri())
+ page.wait_for_timeout(120) # let the SVG paint
+ target = STATIC / f"icon-{size}.png"
+ page.screenshot(path=target, omit_background=True)
+ page.close()
+ print(f"wrote {target} ({target.stat().st_size} bytes)")
+ browser.close()
+ finally:
+ scratch.unlink(missing_ok=True)
if __name__ == "__main__":
- for size in (180, 512):
- target = STATIC / f"icon-{size}.png"
- write_png(target, size, render(size))
- print(f"wrote {target} ({target.stat().st_size} bytes)")
+ main()