Use the HEOS mark, in white, for the header and the icon

The word HEOS at the top of the panel becomes the mark itself, and the
home-screen icons are rendered from the same file so there is one place
to change it. All white on transparent: the panel is dark, and iOS lays
a transparent apple-touch-icon over black.

make_icons.py no longer draws its own glyph -- it rasterises logo.svg
with headless Chromium, which is heavier than the old arithmetic but the
only way to render real paths without a system SVG library. The PNGs are
committed, so it only has to run when the logo changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 21:45:33 +02:00
co-authored by Claude Opus 5
parent 1a1fcc84d6
commit aa1b734b3a
7 changed files with 62 additions and 75 deletions
+1 -1
View File
@@ -186,7 +186,7 @@ GET /avr/raw?cmd=SSFUN ? # any Telnet command, every line back
```bash ```bash
python3 app.py --demo # fake speakers, real interface python3 app.py --demo # fake speakers, real interface
python3 -m unittest discover -s tests -t . # runs against a fake HEOS network 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 ## Layout
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox=".33 -.05 775.27 799.83" fill="#fff">
<title>HEOS</title>
<path d="m653.17 350.24v102.24c22.53 0 40.76-18.33 40.76-40.89v-20.45c0-22.57-18.23-40.9-40.76-40.9zm81.6-61.28v224.8c22.52 0 40.83-18.27 40.83-40.9v-143c0-22.63-18.31-40.9-40.83-40.9zm-652.84 102.18v20.45c0 22.56 18.23 40.89 40.83 40.89v-102.24c-22.6 0-40.83 18.33-40.83 40.9zm-81.6-61.28v143c0 22.63 18.24 40.9 40.76 40.9v-224.8c-22.52 0-40.76 18.27-40.76 40.9z"/>
<path d="m163.52 248.06v306.53c0 45.13 36.55 81.73 81.6 81.73v-469.99c-45.05 0-81.6 36.61-81.6 81.73z"/>
<path d="m530.74 166.33v469.99c45.05 0 81.6-36.6 81.6-81.73v-306.53c0-45.12-36.55-81.73-81.6-81.73z"/>
<path d="m428.73 431.97h-81.6c-22.52 0-40.83 18.33-40.83 40.89v245.26c0 45.12 36.54 81.66 81.66 81.66 45.06 0 81.6-36.54 81.6-81.66v-245.26c0-22.56-18.31-40.89-40.83-40.89z"/>
<path d="m428.73 367.76h-81.6c-22.52 0-40.83-18.34-40.83-40.9v-245.25c0-45.13 36.54-81.66 81.66-81.66 45.06 0 81.6 36.53 81.6 81.66v245.25c0 22.56-18.31 40.9-40.83 40.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+2 -1
View File
@@ -59,7 +59,8 @@ svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width:
/* --- header ---------------------------------------------------------- */ /* --- header ---------------------------------------------------------- */
.top { display: flex; align-items: center; justify-content: space-between; padding: 6px 4px 0; } .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 { .icon-button {
width: 44px; height: 44px; border-radius: 50%; width: 44px; height: 44px; border-radius: 50%;
+1 -1
View File
@@ -21,7 +21,7 @@
<div class="app"> <div class="app">
<header class="top"> <header class="top">
<h1>{{ app_name }}</h1> <h1><img class="logo" src="{{ url_for('static', filename='logo.svg') }}" alt="{{ app_name }}"></h1>
<button class="icon-button" data-role="refresh" aria-label="Refresh"> <button class="icon-button" data-role="refresh" aria-label="Refresh">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 12a8 8 0 1 1-2.34-5.66M20 4v5h-5"/></svg> <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 12a8 8 0 1 1-2.34-5.66M20 4v5h-5"/></svg>
</button> </button>
+50 -72
View File
@@ -1,88 +1,66 @@
#!/usr/bin/env python3 #!/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 python3 tools/make_icons.py
Writes static/icon-180.png (what iOS uses for the home screen) and Writes static/icon-180.png (what iOS uses on the home screen) and
static/icon-512.png (Android / the web manifest). Re-run it if you want static/icon-512.png (Android / the web manifest): the white mark on a
different colours -- they are the two constants below. 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 sys
import struct
import zlib
from pathlib import Path 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" 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 = """<!doctype html>
<style>
html, body {{ margin: 0; background: transparent; }}
body {{ width: {size}px; height: {size}px; display: grid; place-items: center; }}
img {{ width: {inner}px; height: {inner}px; object-fit: contain; }}
</style>
<img src="logo.svg">
"""
def coverage(x: float, y: float, size: float) -> float: def main():
"""How much of the glyph covers this point: a dot plus three arcs try:
opening to the right, i.e. the usual 'sound coming out' mark.""" from playwright.sync_api import sync_playwright
cx, cy = 0.33 * size, 0.5 * size except ImportError:
dx, dy = x - cx, y - cy sys.exit("needs playwright: pip install playwright && playwright install chromium")
distance = math.hypot(dx, dy)
if distance <= 0.075 * size: if not LOGO.exists():
return 1.0 sys.exit(f"no logo at {LOGO}")
angle = abs(math.degrees(math.atan2(dy, dx))) scratch = STATIC / "_icon.html"
if angle > 50: try:
return 0.0 with sync_playwright() as p:
browser = p.chromium.launch(args=["--no-sandbox"])
half = 0.024 * size for size in SIZES:
for radius in (0.17, 0.27, 0.37): inner = round(size * (1 - 2 * MARGIN))
if abs(distance - radius * size) <= half: scratch.write_text(PAGE.format(size=size, inner=inner))
return 1.0 page = browser.new_page(viewport={"width": size, "height": size})
return 0.0 page.goto(scratch.as_uri())
page.wait_for_timeout(120) # let the SVG paint
target = STATIC / f"icon-{size}.png"
def render(size: int) -> bytes: page.screenshot(path=target, omit_background=True)
rows = bytearray() page.close()
step = 1.0 / SAMPLES print(f"wrote {target} ({target.stat().st_size} bytes)")
for py in range(size): browser.close()
rows.append(0) # PNG filter type 0 for this scanline finally:
mix = py / max(1, size - 1) scratch.unlink(missing_ok=True)
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"")
)
if __name__ == "__main__": if __name__ == "__main__":
for size in (180, 512): main()
target = STATIC / f"icon-{size}.png"
write_png(target, size, render(size))
print(f"wrote {target} ({target.stat().st_size} bytes)")