scripts/banner.py builds artwork/banner.svg from the web UI's logo and wordmark, as Jarl's does; scripts/logo.py runs it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
3.5 KiB
Python
Executable File
76 lines
3.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Builds artwork/banner.svg, the README's banner: the logo above the
|
|
# "STEWARD" wordmark and its DHCP rule, laid out as on the web UI's login
|
|
# page. It reads both from the web UI's own sources, so it matches the login
|
|
# page:
|
|
#
|
|
# internal/web/static/assets/logo.js the logo's shapes (from scripts/logo.py)
|
|
# internal/web/static/assets/brand.js the wordmark's letters and width
|
|
#
|
|
# The rule is drawn here with the same numbers as wordmark() in brand.js;
|
|
# keep the two in step. The letters and rule take the UI's text and accent
|
|
# colors, switching to the dark theme's when the viewer prefers dark. Needs
|
|
# only Python; run from the repository root after changing either file
|
|
# (scripts/logo.py runs it):
|
|
#
|
|
# python3 scripts/banner.py
|
|
import json
|
|
import re
|
|
import sys
|
|
|
|
ASSETS = "internal/web/static/assets"
|
|
OUT = "artwork/banner.svg"
|
|
|
|
# The UI's --text and --accent (style.css), light and dark.
|
|
STYLE = (".t{fill:#1d2330}.a{fill:#2f5fb3}.l{fill:none;stroke:#2f5fb3;stroke-width:2.6}"
|
|
".g{fill:#2f5fb3;font:700 17px system-ui,sans-serif;letter-spacing:.5px}"
|
|
"@media (prefers-color-scheme:dark){.t{fill:#e3e7ee}.a,.g{fill:#6d9be8}.l{stroke:#6d9be8}}")
|
|
|
|
|
|
def main():
|
|
logo = open(f"{ASSETS}/logo.js").read()
|
|
view_box = re.search(r'viewBox = "([^"]*)"', logo).group(1)
|
|
shapes = [json.loads(m) for m in re.findall(r"^ (\[.*\]),$", logo, re.M)]
|
|
|
|
brand = open(f"{ASSETS}/brand.js").read()
|
|
letters = re.findall(r'\{ x: (\d+), d: "([^"]*)" \}', brand)
|
|
w = int(re.search(r"^const W = (\d+);", brand, re.M).group(1))
|
|
if len(shapes) < 5 or len(letters) != 7:
|
|
sys.exit("banner.py: logo.js or brand.js no longer looks as expected; update the script")
|
|
|
|
# The login page's sizes in pixels (style.css): a 104px logo, 10px above
|
|
# a 290px wordmark whose viewBox is W+8 units wide.
|
|
mark_w, mark_h = w + 8, 162
|
|
unit = mark_w / 290
|
|
logo_size, gap = round(104 * unit, 1), round(10 * unit, 1)
|
|
height = logo_size + gap + mark_h
|
|
|
|
c = w / 2
|
|
rule = (f'<path class="a" d="M2 18 L{c - 60:g} 15.6 V20.4 Z M{w - 2} 18 L{c + 60:g} 15.6 V20.4 Z"/>'
|
|
f'<circle class="l" cx="{c - 52:g}" cy="18" r="4.6"/>'
|
|
f'<path class="l" d="M{c - 47.4:g} 18 H{c - 30:g} M{c + 30:g} 18 H{c + 47.4:g}"/>'
|
|
f'<circle class="l" cx="{c:g}" cy="18" r="30"/>'
|
|
f'<circle class="l" cx="{c + 52:g}" cy="18" r="4.6"/>'
|
|
f'<text class="g" x="{c:g}" y="24.5" text-anchor="middle">DHCP</text>')
|
|
|
|
def shape(tag, attrs):
|
|
return f"<{tag} " + " ".join(f'{k}="{v}"' for k, v in attrs.items()) + "/>"
|
|
|
|
svg = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {mark_w} {height:g}" role="img" aria-label="Steward">',
|
|
f"<!-- Generated by scripts/banner.py from {ASSETS}/logo.js and brand.js. -->",
|
|
f"<title>Steward</title><style>{STYLE}</style>",
|
|
f'<svg x="{(mark_w - logo_size) / 2:g}" width="{logo_size:g}" height="{logo_size:g}" viewBox="{view_box}">'
|
|
+ "".join(shape(t, a) for t, a in shapes) + "</svg>",
|
|
f'<svg y="{logo_size + gap:g}" width="{mark_w}" height="{mark_h}" viewBox="-4 -4 {mark_w} {mark_h}">'
|
|
'<g class="t" fill-rule="evenodd">'
|
|
+ "".join(f'<path transform="translate({x} 0)" d="{d}"/>' for x, d in letters) + "</g>"
|
|
f'<g transform="translate(0 118)">{rule}</g></svg>',
|
|
"</svg>"]
|
|
with open(OUT, "w") as f:
|
|
f.write("\n".join(svg) + "\n")
|
|
print(f"{OUT}: {mark_w}x{height:g}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|