README banner: the logo above the STEWARD wordmark
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>
This commit is contained in:
1 parent
0179c99cd5
commit
fbad6ef3c4
5 files changed
+94
-6
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
<p align="center"><img src="artwork/logo.svg" width="140" alt="Steward"></p>
|
<p align="center"><img src="artwork/banner.svg" width="340" alt="Steward: a key whose bow is a crown with network nodes, above the STEWARD wordmark"></p>
|
||||||
|
|
||||||
# Steward
|
# Steward
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 556 380.6" role="img" aria-label="Steward">
|
||||||
|
<!-- Generated by scripts/banner.py from internal/web/static/assets/logo.js and brand.js. -->
|
||||||
|
<title>Steward</title><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}}</style>
|
||||||
|
<svg x="178.3" width="199.4" height="199.4" viewBox="30 22 940 940"><path fill="#fff" stroke="#1d5287" stroke-width="26" stroke-linejoin="miter" d="M452 540 V890 L478 940 H522 L548 890 V540 Z"/><path fill="#fff" stroke="#1d5287" stroke-width="26" stroke-linejoin="miter" d="M548 690 H688 V742 H640 V782 H712 V832 H664 V890 H548 Z"/><path fill="#fff" stroke="#1d5287" stroke-width="26" stroke-linejoin="miter" d="M420 450 H580 L606 498 L580 546 H420 L394 498 Z"/><path fill="#fff" stroke="#1d5287" stroke-width="26" stroke-linejoin="miter" d="M305 490 L245 135 L382 238 L500 45 L618 238 L755 135 L695 490 Q500 445 305 490 Z"/><path d="M287 418 Q500 368 713 418" stroke="#1d5287" stroke-width="22" fill="none"/><path stroke="#1d5287" stroke-width="22" fill="none" d="M500 200 V258 M463 312 L414 334 M537 312 L586 334"/><circle stroke="#1d5287" stroke-width="22" fill="#fff" cx="500" cy="170" r="30"/><circle stroke="#1d5287" stroke-width="22" fill="#fff" cx="390" cy="344" r="27"/><circle stroke="#1d5287" stroke-width="22" fill="#fff" cx="610" cy="344" r="27"/><circle cx="500" cy="296" r="40" fill="#1d5287"/></svg>
|
||||||
|
<svg y="218.6" width="556" height="162" viewBox="-4 -4 556 162"><g class="t" fill-rule="evenodd"><path transform="translate(0 0)" d="M10 0 H54 L58 30 L40 16 H18 V41 H48 L58 51 V90 L48 100 H4 L0 70 L18 84 H40 V59 H10 L0 49 V10 Z"/><path transform="translate(70 0)" d="M0 0 H70 L66 22 L52 18 H44 V100 H26 V18 H18 L4 22 Z"/><path transform="translate(152 0)" d="M0 0 H52 L56 18 H18 V41 H44 V59 H18 V82 H40 L58 70 L54 100 H0 Z"/><path transform="translate(222 0)" d="M0 0 H18 L28 66 L38 30 H52 L62 66 L72 0 H90 L76 100 H54 L45 66 L36 100 H14 Z"/><path transform="translate(324 0)" d="M35 0 L46 10 L64 90 L70 100 H50 L44 76 L35 62 L26 76 L20 100 H0 L6 90 L24 10 Z M35 26 L41 40 L35 54 L29 40 Z"/><path transform="translate(406 0)" d="M0 0 H48 L60 12 V40 L50 50 L66 100 H46 L34 60 H18 V100 H0 Z M18 14 H41 L46 19 V35 L41 46 H18 Z"/><path transform="translate(484 0)" d="M0 0 H44 L64 20 V80 L44 100 H0 Z M18 16 H38 L46 24 V76 L38 84 H18 Z"/></g><g transform="translate(0 118)"><path class="a" d="M2 18 L214 15.6 V20.4 Z M546 18 L334 15.6 V20.4 Z"/><circle class="l" cx="222" cy="18" r="4.6"/><path class="l" d="M226.6 18 H244 M304 18 H321.4"/><circle class="l" cx="274" cy="18" r="30"/><circle class="l" cx="326" cy="18" r="4.6"/><text class="g" x="274" y="24.5" text-anchor="middle">DHCP</text></g></svg>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
+7
-3
@@ -24,7 +24,8 @@ scripts/e2e.sh end-to-end test with real DHCP clients in Docker
|
|||||||
scripts/mkdeb.sh the Debian packages (make deb)
|
scripts/mkdeb.sh the Debian packages (make deb)
|
||||||
scripts/logo.py the web UI's logo from artwork/logo.svg
|
scripts/logo.py the web UI's logo from artwork/logo.svg
|
||||||
scripts/optionsdoc.py docs/options.md from the option catalog
|
scripts/optionsdoc.py docs/options.md from the option catalog
|
||||||
artwork/logo.svg the logo's master
|
scripts/banner.py the README's banner from the web UI's logo and wordmark
|
||||||
|
artwork/logo.svg the logo's master; artwork/banner.svg the README's banner
|
||||||
```
|
```
|
||||||
|
|
||||||
[design.md](design.md) describes how the pieces fit.
|
[design.md](design.md) describes how the pieces fit.
|
||||||
@@ -83,8 +84,11 @@ same helpers as Jarl's:
|
|||||||
|
|
||||||
The logo's master is `artwork/logo.svg`; `scripts/logo.py` writes
|
The logo's master is `artwork/logo.svg`; `scripts/logo.py` writes
|
||||||
`assets/logo.svg` (the favicon) and `assets/logo.js` (drawn inline by
|
`assets/logo.svg` (the favicon) and `assets/logo.js` (drawn inline by
|
||||||
`brand.js`). The STEWARD wordmark is drawn by `wordmark()` in `brand.js`,
|
`brand.js`), and then runs `scripts/banner.py`. The STEWARD wordmark is
|
||||||
in the letters of Jarl's wordmark.
|
drawn by `wordmark()` in `brand.js`, in the letters of Jarl's wordmark.
|
||||||
|
`artwork/banner.svg`, the README's banner, is the login page's logo and
|
||||||
|
wordmark, generated by `scripts/banner.py`. It draws the rule with the same
|
||||||
|
numbers as `brand.js`, so change both together.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
Executable
+75
@@ -0,0 +1,75 @@
|
|||||||
|
#!/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())
|
||||||
+5
-2
@@ -8,13 +8,15 @@
|
|||||||
# logo appears together with the page
|
# logo appears together with the page
|
||||||
#
|
#
|
||||||
# The master is hand-written SVG: <g> elements whose attributes apply to
|
# The master is hand-written SVG: <g> elements whose attributes apply to
|
||||||
# their <path> and <circle> children. Run from the repository root after
|
# their <path> and <circle> children. It then runs scripts/banner.py to
|
||||||
# changing it:
|
# rebuild the README's banner. Run from the repository root after changing
|
||||||
|
# it:
|
||||||
#
|
#
|
||||||
# python3 scripts/logo.py
|
# python3 scripts/logo.py
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
@@ -51,6 +53,7 @@ def main():
|
|||||||
shutil.copyfile(MASTER, os.path.join(OUT, "logo.svg"))
|
shutil.copyfile(MASTER, os.path.join(OUT, "logo.svg"))
|
||||||
for name in ("logo.svg", "logo.js"):
|
for name in ("logo.svg", "logo.js"):
|
||||||
print(f"{OUT}/{name}: {os.path.getsize(os.path.join(OUT, name))} bytes")
|
print(f"{OUT}/{name}: {os.path.getsize(os.path.join(OUT, name))} bytes")
|
||||||
|
subprocess.run([sys.executable, "scripts/banner.py"], check=True)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in new issue
Block a user