Files
jarl/scripts/logo.py
T
abrown38andClaude Opus 5.5 a7d389be27 Web UI: Jarl logo and wordmark
Add the logo, a Viking jarl drawn like a woodcut whose spear-pointed crown
holds a DNS tree. It is generated by scripts/logo.py, which mirrors the
left half so the figure is exactly symmetrical, as logo.svg (white on a
Nordic-blue disc) and logo-white.svg. It links home in the header, sits on
the login page and is the favicon.

The login page shows an angular "JARL" wordmark drawn as SVG paths over a
rule with a "DNS" node, coloured from the theme. The header's "Jarl" uses
a self-hosted Cinzel Bold subset (SIL OFL); jarld serves .woff2 as
font/woff2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:19:51 +00:00

71 lines
4.5 KiB
Python
Executable File

#!/usr/bin/env python3
# Generates the Jarl logo: internal/web/static/assets/logo.svg (white figure
# on a Nordic-blue disc) and logo-white.svg (the figure alone, for dark
# backgrounds). Shapes are given for the left half and the centre line and
# mirrored, so the figure is exactly symmetrical. Run from the repository
# root: python3 scripts/logo.py
import re, sys
def mirror_path(d):
toks = re.findall(r'[MLQCZHV]|-?\d+(?:\.\d+)?', d)
out=[]; cmd=None; nums=[]
def flush():
if cmd is None: return
if cmd in 'MLQC':
v=[float(n) for n in nums]
out.append(cmd+" "+" ".join(f"{128-v[i]:g} {v[i+1]:g}" for i in range(0,len(v),2)))
elif cmd=='H': out.append("H "+" ".join(f"{128-float(n):g}" for n in nums))
elif cmd=='V': out.append("V "+" ".join(nums))
elif cmd=='Z': out.append("Z")
for t in toks:
if t.isalpha(): flush(); cmd=t; nums=[]
else: nums.append(t)
flush()
return " ".join(out)
LEFT = ['M33 46 L37 46 Q35 64 30 78 Q26 88 24 100 Q22 84 26 72 Q31 60 33 46 Z', 'M38 47 L42 47 Q41 66 37 82 Q34 92 33 104 Q31 88 34 76 Q37 62 38 47 Z', 'M41 51 L44.5 55 L44 63 L41.5 59 Z', 'M47 58 Q52.5 55 58 59.5 Q52.5 63 47 58 Z M51.4 59 L53 57.4 L54.6 59 L53 60.6 Z', 'M45 61.5 L54.5 65 L57.5 72.5 L50 73 L45.5 67.5 Z', 'M63 76 L57 75 L48 78 Q42 82 38 92 Q45 85 50 83 L57 80.5 L63 80 Z']
CENTER = ['M40 46 H88 L86.5 50 L71.5 55.5 L66.5 62.5 L61.5 62.5 L56.5 55.5 L41.5 50 Z', 'M61.5 63 H66.5 L69 72.5 L64 75.5 L59 72.5 Z', 'M38 69 L45 73.5 L63 79.5 H65 L83 73.5 L90 69 Q95 84 92 96 L88 104 L86 101 L83 110 L80 106 L76 116 L73 111 L69 121 L66 116 L64 127 L62 116 L59 121 L55 111 L52 116 L48 106 L45 110 L42 101 L40 104 L36 96 Q33 84 38 69 Z']
CUTS = [('M40 80 C42.5 86 37 91 39.5 99 C40.5 102 39 104 40 106', 2.0), ('M44.5 86 C47 92 42 97 44.5 106', 1.7), ('M48.5 85.5 C50.5 90 47 94 48.5 99', 1.5), ('M50.5 92 C53 99 48.5 104 51 113', 2.0), ('M54.5 86.5 C57 94 52.5 101 55 109', 1.7), ('M58 95 C59.5 101 56.5 106 58.5 116', 1.8), ('M60.8 85.5 C62.3 92 59.8 98 61.2 105', 1.5), ('M46 101.5 C47 104 45.5 106.5 46.5 109.5', 1.4), ('M62.2 109 C62.7 113 61.7 116 62.4 119.5', 1.4), ('M56.5 77.6 Q49.5 79.2 43 85.5', 1.3), ('M58.6 81.6 Q49.5 83.8 41.5 95', 2.4)]
CENTER_CUTS = [('M60.4 82.2 H67.6', 1.8)]
CROWN = "M36 44 L35 28 L32 18 L41 25 L50 30 L55 21 L64 9 L73 21 L78 30 L87 25 L96 18 L93 28 L92 44 Z"
def figure(color="#fff", mask_id="cut"):
shapes = CENTER + [p for d in LEFT for p in (d, mirror_path(d))]
cuts = [(p, w) for d, w in CUTS for p in (d, mirror_path(d))] + CENTER_CUTS
mask = (f'<mask id="{mask_id}" maskUnits="userSpaceOnUse" x="0" y="0" width="128" height="128">'
'<rect width="128" height="128" fill="#fff"/>'
'<g fill="none" stroke="#000" stroke-linecap="round">' + "".join(f'<path stroke-width="{w}" d="{c}"/>' for c, w in cuts) + '</g></mask>')
return f'''<path fill="none" stroke="{color}" stroke-width="3.6" stroke-linejoin="miter" stroke-miterlimit="8" d="{CROWN}"/>
<path fill="{color}" d="M37 37 H91 V40.5 H37 Z"/>
<path fill="{color}" d="M32 18 L27.5 9 L37 13.5 Z M96 18 L100.5 9 L91 13.5 Z M64 9 L61 3.5 L64 0.5 L67 3.5 Z"/>
<g fill="none" stroke="{color}" stroke-width="2.3"><circle cx="64" cy="19" r="3"/><path d="M64 22 V28.5 M55.3 31.5 H72.7"/><circle cx="52.2" cy="31.5" r="3"/><circle cx="75.8" cy="31.5" r="3"/></g>
<circle cx="64" cy="31.5" r="3.2" fill="{color}"/>
<defs>{mask}</defs>
<g fill="{color}" fill-rule="evenodd" mask="url(#{mask_id})">
''' + "\n".join(f' <path d="{d}"/>' for d in shapes) + "\n </g>"
DESC = """<!-- Jarl: a Viking ruler in a spear-pointed crown, drawn like a woodcut.
The crown holds a DNS tree: the root node with its branches below.
Generated by scripts/logo.py, which mirrors the left half, so it is
exactly symmetrical; the beard's strands are cut out with a mask. -->"""
def main(outdir):
logo = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-labelledby="t">
<title id="t">Jarl</title>
{DESC}
<circle cx="64" cy="64" r="64" fill="#2f5fb3"/>
<g transform="translate(64 66) scale(0.8) translate(-64 -64)">
{figure("#fff", "jarl-cut")}
</g>
</svg>
'''
white = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="20 -1 88 130" role="img" aria-labelledby="t">
<title id="t">Jarl</title>
{DESC}
{figure("#fff", "jarl-cut-white")}
</svg>
'''
open(outdir + "/logo.svg", "w").write(logo)
open(outdir + "/logo-white.svg", "w").write(white)
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "internal/web/static/assets")