From aa1b734b3a7c8ca033737541d4513e878d243ac9 Mon Sep 17 00:00:00 2001 From: Franzz Date: Mon, 14 Sep 2026 21:45:33 +0200 Subject: [PATCH] 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 --- README.md | 2 +- static/icon-180.png | Bin 2674 -> 2392 bytes static/icon-512.png | Bin 7062 -> 7375 bytes static/logo.svg | 8 +++ static/panel.css | 3 +- templates/index.html | 2 +- tools/make_icons.py | 122 ++++++++++++++++++------------------------- 7 files changed, 62 insertions(+), 75 deletions(-) create mode 100644 static/logo.svg 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 b374d6d1b09be167d2c125fe19048962a7f684a8..38fe83adcc41e831c5ebf762fb2f213980583117 100644 GIT binary patch literal 2392 zcmbW3X*3%M7ls+N)RJgXTM*QzY6Pt!_Fb)=qAEp+CDE~k)D~M~OYBHYJMk>EAiuk2&+>-h0ly|K4-Y`#!0^*;@+mO7OC=u?bjRH*+|h z4L^&A>$K|+b6sI$J1byiX6zJMv^f`5E%iZ)duQ9uU-F`+1+hnj81DG7MU&=iC$Cu{ z5P0rmy!8unPOY5g1aKKRLGotjoFTt@8eiJQ-`$o|Tr)0-X*tb$W$ZoR1sFz;Up*igh-qqYW3KZvH8dmY_j*vDk;A^E>27OuDekS> z4PU09Khnt?>hFNG`x`3@;&1fX-<+kb)Yj zus$ktS7=HeH1n~0wd6!Y%yoF)a z3wHoe%|wx9mHQ#Ca#{C|Mjh8k7LPdGXIJq^Nf+66?V4sK{8|;s1hl^F^*Q2*%^8ma z{WBGaVV&~qJt@d!Oyx9V3wI_2*5$+t7oYb^hUH&C!=4ZlbwvEaWA@Dt z3>o}X=kjCBwTWGCYIq`VMA_(H_3a~j!c@7G*6ghHp`1i1P@|`^%kAY>xU+B zlybExG)!)6bF7%AH!>qI$F!t|)+KI@v46!PERR9-`|gwD?z$xDr51dcp^tY3hrSph z$x2WwIT=pG2$-c6eYrNKo?#KjN733Swp9<)tpVsdaZpqO^g14ZKR7O^S23*fR^l-qjDmlW z9K9e|A>rq7LD!)F-n-c^e^X9^Ph-?+Yp5lESB!DqWrBu#O76-$BT5c8GnF{2A-b=v zsSLA}Yqi?7Bu&{NwvsoyszmAUElb(QIfgm)Md)DznQ_1uv+D+JNwKlQa_JHmA&b%h z_PDU#n7!|#f;Tr+yqjz1frpz}sI6T+GvUK2mI3r-Jg1HOU72I?bG?9uE;P%)LQh6E zRu^2>=vMT2!hT5^zvf#Z{}QNJ&gsz=div700!mDOlq`1aLWqLgEk%4cJDBj~*9v1o zGfFG6r8UH>FczuA91TJ0KRB2jsb$J(Br(Dj7(rBVV(R>_<7rZdK8<5;WO(rRC?DK0 zg3wv$Dc(31g@$PnFi*cSPtp)}-dm%*ik&5&TApLgE|MMlL=F@+BbGi!z%(#=g#_34 ztKhgMwSwUewWD)IS}uJ1Zz2d$^3z*dlQkhw)^bENCWF=B^%w2HmDDU(MS8=oLh;2H z$LJR_2r5H{u^flXT5j{B<6cVqP=G}f5)hc~As4-D{GyWN*c?2F;G;&XLnYHX#u65h{4f42H{B+v!PO8XH-*_yHu)o9YpAH-g zRRVqLYZdIXgl^BA?cw_-7+OG_daf)26RAYufzMuOJxT;!ZfO0PI0Gh;joC9!H~S2ugWs= z`2$4eW_BU0wH`RHyi@Y7A+C?yxmXn$AHEmR7%lgi61&i~y#v%#EHhZ!S%}0B9`QU2 zfz2Kkk6ZvEBSvp|dtn`iV!DA5wuRhP*%uW8uZ^y}ladHfs`$km|d#|ebo|z}{ z{Dzc)?wmsxTqrDl?vbU6WjSUe+IDm0d%!B|@W(LybJk`QwKi^LTN`517=jU^XAoq{ z%5}uo1-Q`C3#mOJ(7Ke^?rGD1kdl>?)1i-rnR^O`QbaI!OY>{|TC9sK;C_fY()Ur( zT(!Nv3q0232bG1P*qugD6{)DVdSQ#y1zHacGXHTjHFsWbJp5k17Gs(0LOe)^$ToVN1H2O5tbz>Y@brCMdS9Lf7k^`Q-yo zcm9GvNotTnWS{yOE>%!&N#^faqBxi_5S5Sn%Ltgt&hNVJ?qc)X-Te7*tvD(liJ(0u z!sB|&IbSyj;O~oVN>ShEZ)bH7cS(dOes_+nT9$<-blHGd3!8kX&-4{wgi)_hmpiF* zmsd{d0SLAI_rUP~Fyp%f8{5fA6-DvoDIanD1S1Z%+pJG{Ob?jJ>4we9+}^Ct#53W4 D9TSZ4 literal 2674 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 diff --git a/static/icon-512.png b/static/icon-512.png index c3ee4a203e67f6ac850d1add9610b6919cc0402b..b1c66c2b6e9f758a77ef49db8aab5d1868e6fec4 100644 GIT binary patch literal 7375 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 + 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()