feat: draw the site mark and favicons as vectors

The header logo was a 256 px compressed raster whose navy chip vanished on the indigo bar. The chip is now an SVG, white with the glyph cut out, and the favicon set (SVG, 32 px and an opaque 180 px touch icon) comes from scripts/make_site_icons.py.
This commit is contained in:
Abdessamad Derraz committed 2026-10-22 12:34:44 +02:00
1 parent f67f4ead1b
commit 427a8183e3
8 files changed
+146 -4

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><style>.c{fill:#3b3f7a}@media (prefers-color-scheme: dark){.c{fill:#8a8fd6}}</style><rect x="11.8" y="2.5" width="3.4" height="5.5" rx="0.8" class="c"/><rect x="11.8" y="40" width="3.4" height="5.5" rx="0.8" class="c"/><rect x="2.5" y="11.8" width="5.5" height="3.4" rx="0.8" class="c"/><rect x="40" y="11.8" width="5.5" height="3.4" rx="0.8" class="c"/><rect x="18.8" y="2.5" width="3.4" height="5.5" rx="0.8" class="c"/><rect x="18.8" y="40" width="3.4" height="5.5" rx="0.8" class="c"/><rect x="2.5" y="18.8" width="5.5" height="3.4" rx="0.8" class="c"/><rect x="40" y="18.8" width="5.5" height="3.4" rx="0.8" class="c"/><rect x="25.8" y="2.5" width="3.4" height="5.5" rx="0.8" class="c"/><rect x="25.8" y="40" width="3.4" height="5.5" rx="0.8" class="c"/><rect x="2.5" y="25.8" width="5.5" height="3.4" rx="0.8" class="c"/><rect x="40" y="25.8" width="5.5" height="3.4" rx="0.8" class="c"/><rect x="32.8" y="2.5" width="3.4" height="5.5" rx="0.8" class="c"/><rect x="32.8" y="40" width="3.4" height="5.5" rx="0.8" class="c"/><rect x="2.5" y="32.8" width="5.5" height="3.4" rx="0.8" class="c"/><rect x="40" y="32.8" width="5.5" height="3.4" rx="0.8" class="c"/><rect x="8" y="8" width="32" height="32" rx="4" class="c"/><rect x="11.6" y="11.6" width="5" height="5" rx="0.6" fill="#8be0f7"/><rect x="18.2" y="11.6" width="5" height="5" rx="0.6" fill="#8be0f7"/><rect x="24.8" y="11.6" width="5" height="5" rx="0.6" fill="#4fa3c4"/><rect x="18.2" y="18.2" width="5" height="5" rx="0.6" fill="#8be0f7"/><rect x="31.4" y="18.2" width="5" height="5" rx="0.6" fill="#4fa3c4"/><rect x="11.6" y="24.8" width="5" height="5" rx="0.6" fill="#8be0f7"/><rect x="31.4" y="24.8" width="5" height="5" rx="0.6" fill="#8be0f7"/><rect x="18.2" y="31.4" width="5" height="5" rx="0.6" fill="#4fa3c4"/><rect x="24.8" y="31.4" width="5" height="5" rx="0.6" fill="#8be0f7"/><rect x="31.4" y="31.4" width="5" height="5" rx="0.6" fill="#8be0f7"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" role="img" aria-label="RetroBIOS"><mask id="glyph"><rect x="8" y="8" width="32" height="32" rx="4" fill="#fff"/><rect x="11.6" y="11.6" width="5" height="5" rx="0.6" fill="#000"/><rect x="18.2" y="11.6" width="5" height="5" rx="0.6" fill="#000"/><rect x="24.8" y="11.6" width="5" height="5" rx="0.6" fill="#5a5a5a"/><rect x="18.2" y="18.2" width="5" height="5" rx="0.6" fill="#000"/><rect x="31.4" y="18.2" width="5" height="5" rx="0.6" fill="#5a5a5a"/><rect x="11.6" y="24.8" width="5" height="5" rx="0.6" fill="#000"/><rect x="31.4" y="24.8" width="5" height="5" rx="0.6" fill="#000"/><rect x="18.2" y="31.4" width="5" height="5" rx="0.6" fill="#5a5a5a"/><rect x="24.8" y="31.4" width="5" height="5" rx="0.6" fill="#000"/><rect x="31.4" y="31.4" width="5" height="5" rx="0.6" fill="#000"/></mask><rect x="11.8" y="2.5" width="3.4" height="5.5" rx="0.8" fill="#fff"/><rect x="11.8" y="40" width="3.4" height="5.5" rx="0.8" fill="#fff"/><rect x="2.5" y="11.8" width="5.5" height="3.4" rx="0.8" fill="#fff"/><rect x="40" y="11.8" width="5.5" height="3.4" rx="0.8" fill="#fff"/><rect x="18.8" y="2.5" width="3.4" height="5.5" rx="0.8" fill="#fff"/><rect x="18.8" y="40" width="3.4" height="5.5" rx="0.8" fill="#fff"/><rect x="2.5" y="18.8" width="5.5" height="3.4" rx="0.8" fill="#fff"/><rect x="40" y="18.8" width="5.5" height="3.4" rx="0.8" fill="#fff"/><rect x="25.8" y="2.5" width="3.4" height="5.5" rx="0.8" fill="#fff"/><rect x="25.8" y="40" width="3.4" height="5.5" rx="0.8" fill="#fff"/><rect x="2.5" y="25.8" width="5.5" height="3.4" rx="0.8" fill="#fff"/><rect x="40" y="25.8" width="5.5" height="3.4" rx="0.8" fill="#fff"/><rect x="32.8" y="2.5" width="3.4" height="5.5" rx="0.8" fill="#fff"/><rect x="32.8" y="40" width="3.4" height="5.5" rx="0.8" fill="#fff"/><rect x="2.5" y="32.8" width="5.5" height="3.4" rx="0.8" fill="#fff"/><rect x="40" y="32.8" width="5.5" height="3.4" rx="0.8" fill="#fff"/><rect x="8" y="8" width="32" height="32" rx="4" fill="#fff" mask="url(#glyph)"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+6
View File
@@ -29,6 +29,12 @@
{% else %}{% set section = 'reference' %}{% endif %}
{% set preview_image = config.site_url ~ 'assets/og/' ~ section ~ '.png' if config.site_url else '' %}
{# The theme's favicon is the 32 px PNG. Browsers that read an SVG icon
take this one, listed after it, and a home-screen shortcut takes the
opaque 180 px icon (scripts/make_site_icons.py makes all three). #}
<link rel="icon" type="image/svg+xml" href="{{ 'assets/brand/favicon.svg' | url }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ 'assets/brand/apple-touch-icon.png' | url }}">
<meta property="og:type" content="{{ 'website' if not url else 'article' }}">
<meta property="og:site_name" content="{{ config.site_name }}">
<meta property="og:title" content="{{ page_title }}">
+2 -2
View File
@@ -39,8 +39,8 @@ theme:
icon: material/brightness-4
name: Switch to auto
font: false
logo: assets/images/logo.png
favicon: assets/images/favicon.png
logo: assets/brand/logo.svg
favicon: assets/brand/favicon-32.png
icon:
logo: material/chip
features:
+2 -2
View File
@@ -6194,8 +6194,8 @@ theme:
icon: material/brightness-4
name: Switch to auto
font: false
logo: assets/images/logo.png
favicon: assets/images/favicon.png
logo: assets/brand/logo.svg
favicon: assets/brand/favicon-32.png
icon:
logo: material/chip
features:
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""Write the site's own mark: header logo, favicon and touch icon.
The mark is the chip of the project logo drawn as vector shapes: a body,
four pins per side and the pixel glyph on a 4 by 4 grid. Three variants:
- ``logo.svg``: white, the glyph cut out so the header colour shows through.
It sits on the indigo header in both schemes.
- ``favicon.svg``: indigo chip with a cyan glyph, lighter in a dark browser.
- ``favicon-32.png`` and ``apple-touch-icon.png`` (180 px, opaque): the same
shapes rasterised by a headless Chromium through Playwright, for browsers
that take no SVG icon and for home-screen shortcuts.
The files land in ``docs_assets/overrides/assets/brand/``, which MkDocs copies
with the theme. The site build never runs this script.
python scripts/make_site_icons.py
"""
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "docs_assets" / "overrides" / "assets" / "brand"
HEADER = "#3b3f7a"
GLYPH = "#8be0f7"
GLYPH_DIM = "#4fa3c4"
# (column, row, dim) on the 4 by 4 grid, read from the project logo.
PIXELS = [
(0, 0, False), (1, 0, False), (2, 0, True),
(1, 1, False), (3, 1, True),
(0, 2, False), (3, 2, False),
(1, 3, True), (2, 3, False), (3, 3, False),
]
BODY = (8, 8, 32, 4) # x, y, size, corner radius in a 48 unit box
PIN_CENTRES = (13.5, 20.5, 27.5, 34.5)
PIN_WIDTH = 3.4
PIN_LENGTH = 5.5
CELL = 5.0
GAP = 1.6
def _pins(fill: str) -> str:
half = PIN_WIDTH / 2
rects = []
for centre in PIN_CENTRES:
start = centre - half
rects += [
(start, 8 - PIN_LENGTH, PIN_WIDTH, PIN_LENGTH),
(start, 40, PIN_WIDTH, PIN_LENGTH),
(8 - PIN_LENGTH, start, PIN_LENGTH, PIN_WIDTH),
(40, start, PIN_LENGTH, PIN_WIDTH),
]
return "".join(
f'<rect x="{x:g}" y="{y:g}" width="{w:g}" height="{h:g}" rx="0.8" fill="{fill}"/>'
for x, y, w, h in rects
)
def _glyph(bright: str, dim: str) -> str:
span = 4 * CELL + 3 * GAP
origin = BODY[0] + (BODY[2] - span) / 2
return "".join(
f'<rect x="{origin + c * (CELL + GAP):g}" y="{origin + r * (CELL + GAP):g}" '
f'width="{CELL:g}" height="{CELL:g}" rx="0.6" fill="{dim if is_dim else bright}"/>'
for c, r, is_dim in PIXELS
)
def _body(fill: str, extra: str = "") -> str:
x, y, size, radius = BODY
return f'<rect x="{x}" y="{y}" width="{size}" height="{size}" rx="{radius}" fill="{fill}"{extra}/>'
def logo_svg() -> str:
"""White chip, glyph cut out: the header colour shows through it."""
return (
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" role="img" aria-label="RetroBIOS">'
'<mask id="glyph">' + _body("#fff") + _glyph("#000", "#5a5a5a") + "</mask>"
+ _pins("#fff") + _body("#fff", ' mask="url(#glyph)"') + "</svg>"
)
def favicon_svg() -> str:
return (
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">'
"<style>.c{fill:" + HEADER + "}"
"@media (prefers-color-scheme: dark){.c{fill:#8a8fd6}}</style>"
+ _pins(HEADER).replace(f'fill="{HEADER}"', 'class="c"')
+ _body(HEADER).replace(f'fill="{HEADER}"', 'class="c"')
+ _glyph(GLYPH, GLYPH_DIM) + "</svg>"
)
def touch_icon_svg() -> str:
"""Opaque square: iOS fills transparency with black."""
return (
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6 -6 60 60">'
f'<rect x="-6" y="-6" width="60" height="60" fill="{HEADER}"/>'
+ logo_svg().split(">", 1)[1].rsplit("</svg>", 1)[0] + "</svg>"
)
def main() -> None:
try:
from playwright.sync_api import sync_playwright
except ImportError as exc:
raise SystemExit("Playwright with Chromium is needed: pip install playwright") from exc
OUT.mkdir(parents=True, exist_ok=True)
for name, text in (("logo.svg", logo_svg()), ("favicon.svg", favicon_svg())):
(OUT / name).write_text(text + "\n", encoding="utf-8")
print((OUT / name).relative_to(ROOT))
rasters = (
("favicon-32.png", 32, favicon_svg().replace("@media (prefers-color-scheme: dark)", "@media not all")),
("apple-touch-icon.png", 180, touch_icon_svg()),
)
with sync_playwright() as playwright:
browser = playwright.chromium.launch()
for name, size, text in rasters:
page = browser.new_page(viewport={"width": size, "height": size})
sized = text.replace("<svg ", f'<svg width="{size}" height="{size}" ', 1)
page.set_content(f'<html><body style="margin:0;background:transparent">{sized}</body></html>')
page.screenshot(path=str(OUT / name), omit_background=True, clip={"x": 0, "y": 0, "width": size, "height": size})
page.close()
print((OUT / name).relative_to(ROOT))
browser.close()
if __name__ == "__main__":
main()