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:
@@ -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 |
@@ -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
@@ -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%;
|
||||||
|
|||||||
@@ -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
@@ -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)")
|
|
||||||
|
|||||||
Reference in New Issue
Block a user