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 @@ + + HEOS + + + + + + 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()