4e115086e6
Signed-off-by: Justin Paul <justin@jpaul.me>
190 lines
6.7 KiB
Python
190 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate Provenance brand assets as portable, font-independent SVGs."""
|
|
import os
|
|
from matplotlib.textpath import TextPath
|
|
from matplotlib.font_manager import FontProperties
|
|
from matplotlib.path import Path
|
|
|
|
SERIF = "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf"
|
|
SANS = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"
|
|
|
|
INK = "#1A1A17"
|
|
INK_DARK = "#F2EEE6"
|
|
BRONZE = "#A06A42"
|
|
PAPER = "#F7F3EC"
|
|
MUTED = "#6B6862"
|
|
MUTED_DARK = "#9A968E"
|
|
|
|
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
|
os.makedirs(OUT, exist_ok=True)
|
|
|
|
|
|
def text_to_path(s, font_path, size):
|
|
"""Return (d_string, width, height). Coordinates: top-left origin, y down."""
|
|
fp = FontProperties(fname=font_path)
|
|
tp = TextPath((0, 0), s, size=size, prop=fp)
|
|
verts, codes = tp.vertices, tp.codes
|
|
xs = verts[:, 0]
|
|
ys = verts[:, 1]
|
|
xmin, xmax = xs.min(), xs.max()
|
|
ymin, ymax = ys.min(), ys.max()
|
|
|
|
def tx(x):
|
|
return round(float(x - xmin), 2)
|
|
|
|
def ty(y):
|
|
return round(float(ymax - y), 2)
|
|
|
|
d = []
|
|
i = 0
|
|
n = len(codes)
|
|
while i < n:
|
|
c = codes[i]
|
|
if c == Path.MOVETO:
|
|
x, y = verts[i]
|
|
d.append(f"M{tx(x)} {ty(y)}")
|
|
i += 1
|
|
elif c == Path.LINETO:
|
|
x, y = verts[i]
|
|
d.append(f"L{tx(x)} {ty(y)}")
|
|
i += 1
|
|
elif c == Path.CURVE3:
|
|
x1, y1 = verts[i]
|
|
x2, y2 = verts[i + 1]
|
|
d.append(f"Q{tx(x1)} {ty(y1)} {tx(x2)} {ty(y2)}")
|
|
i += 2
|
|
elif c == Path.CURVE4:
|
|
x1, y1 = verts[i]
|
|
x2, y2 = verts[i + 1]
|
|
x3, y3 = verts[i + 2]
|
|
d.append(f"C{tx(x1)} {ty(y1)} {tx(x2)} {ty(y2)} {tx(x3)} {ty(y3)}")
|
|
i += 3
|
|
elif c == Path.CLOSEPOLY:
|
|
d.append("Z")
|
|
i += 1
|
|
else:
|
|
i += 1
|
|
return "".join(d), round(float(xmax - xmin), 2), round(float(ymax - ymin), 2)
|
|
|
|
|
|
def origin_mark(cx, cy, R, conn_sw=1.5, tick_sw=2):
|
|
"""Origin/survey-datum mark primitives as an SVG fragment string.
|
|
Ink elements get class='ink'; bronze elements class='br'."""
|
|
import math
|
|
diag = R * 0.7071
|
|
nodes = [(cx + diag, cy - diag), (cx - diag, cy - diag),
|
|
(cx + diag, cy + diag), (cx - diag, cy + diag)]
|
|
parts = []
|
|
# ring (bronze)
|
|
parts.append(f'<circle cx="{cx}" cy="{cy}" r="{R}" class="br-s" '
|
|
f'fill="none" stroke-width="{tick_sw}"/>')
|
|
# cardinal ticks (bronze) crossing the ring
|
|
t_in, t_out = R - 4, R + 4
|
|
for (dx, dy) in [(0, -1), (0, 1), (1, 0), (-1, 0)]:
|
|
x1, y1 = cx + dx * t_in, cy + dy * t_in
|
|
x2, y2 = cx + dx * t_out, cy + dy * t_out
|
|
parts.append(f'<line x1="{round(x1,2)}" y1="{round(y1,2)}" '
|
|
f'x2="{round(x2,2)}" y2="{round(y2,2)}" class="br-s" '
|
|
f'stroke-width="{tick_sw}" stroke-linecap="round"/>')
|
|
# connectors (ink) center -> diagonal nodes
|
|
for (nx, ny) in nodes:
|
|
parts.append(f'<line x1="{cx}" y1="{cy}" x2="{round(nx,2)}" '
|
|
f'y2="{round(ny,2)}" class="ink-s" fill="none" '
|
|
f'stroke-width="{conn_sw}"/>')
|
|
# diagonal nodes (ink)
|
|
nr = max(2.0, R * 0.105)
|
|
for (nx, ny) in nodes:
|
|
parts.append(f'<circle cx="{round(nx,2)}" cy="{round(ny,2)}" '
|
|
f'r="{round(nr,2)}" class="ink"/>')
|
|
# center dot (ink)
|
|
parts.append(f'<circle cx="{cx}" cy="{cy}" r="{round(R*0.15,2)}" '
|
|
f'class="ink"/>')
|
|
return "\n".join(parts)
|
|
|
|
|
|
STYLE = f""".ink{{fill:{INK}}}.ink-s{{stroke:{INK}}}.br{{fill:{BRONZE}}}.br-s{{stroke:{BRONZE}}}.muted{{fill:{MUTED}}}
|
|
@media (prefers-color-scheme:dark){{.ink{{fill:{INK_DARK}}}.ink-s{{stroke:{INK_DARK}}}.muted{{fill:{MUTED_DARK}}}}}"""
|
|
|
|
# ---- 1. Primary horizontal lockup ----
|
|
WM_SIZE = 64
|
|
TAG_SIZE = 15
|
|
wm_d, wm_w, wm_h = text_to_path("Provenance", SERIF, WM_SIZE)
|
|
tag_d, tag_w, tag_h = text_to_path("where it came from matters", SANS, TAG_SIZE)
|
|
|
|
pad = 28
|
|
R = 30
|
|
mark_box = 2 * (R + 6)
|
|
gap = 26
|
|
block_gap = 12
|
|
text_block_h = wm_h + block_gap + tag_h
|
|
content_h = max(mark_box, text_block_h)
|
|
H = round(pad * 2 + content_h, 2)
|
|
mark_cx = pad + (R + 6)
|
|
mark_cy = round(H / 2, 2)
|
|
text_x = pad + mark_box + gap
|
|
W = round(text_x + max(wm_w, tag_w) + pad, 2)
|
|
# vertically center the text block
|
|
block_top = (H - text_block_h) / 2
|
|
wm_y = round(block_top, 2)
|
|
tag_y = round(block_top + wm_h + block_gap, 2)
|
|
|
|
logo = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}" role="img" aria-label="Provenance">
|
|
<title>Provenance</title>
|
|
<style>{STYLE}</style>
|
|
{origin_mark(mark_cx, mark_cy, R)}
|
|
<g transform="translate({text_x} {wm_y})"><path d="{wm_d}" class="ink"/></g>
|
|
<g transform="translate({round(text_x+0.5,2)} {tag_y})"><path d="{tag_d}" class="muted"/></g>
|
|
</svg>
|
|
'''
|
|
open(f"{OUT}/provenance-logo.svg", "w").write(logo)
|
|
|
|
# ---- 1b. Lockup without tagline ----
|
|
H2 = round(pad * 2 + max(mark_box, wm_h), 2)
|
|
mcy2 = round(H2 / 2, 2)
|
|
wm_y2 = round((H2 - wm_h) / 2, 2)
|
|
W2 = round(text_x + wm_w + pad, 2)
|
|
logo2 = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{W2}" height="{H2}" viewBox="0 0 {W2} {H2}" role="img" aria-label="Provenance">
|
|
<title>Provenance</title>
|
|
<style>{STYLE}</style>
|
|
{origin_mark(mark_cx, mcy2, R)}
|
|
<g transform="translate({text_x} {wm_y2})"><path d="{wm_d}" class="ink"/></g>
|
|
</svg>
|
|
'''
|
|
open(f"{OUT}/provenance-logo-plain.svg", "w").write(logo2)
|
|
|
|
# ---- 2. Mark only (square) ----
|
|
S = 96
|
|
c = S / 2
|
|
markonly = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{S}" height="{S}" viewBox="0 0 {S} {S}" role="img" aria-label="Provenance mark">
|
|
<title>Provenance mark</title>
|
|
<style>{STYLE}</style>
|
|
{origin_mark(c, c, 34, conn_sw=1.6, tick_sw=2.2)}
|
|
</svg>
|
|
'''
|
|
open(f"{OUT}/provenance-mark.svg", "w").write(markonly)
|
|
|
|
# ---- 3. App icon / monogram tile (square) ----
|
|
def monogram(side, radius_ratio=0.22):
|
|
p_size = side * 0.62
|
|
pd, pw, ph = text_to_path("P", SERIF, p_size)
|
|
px = round((side - pw) / 2, 2)
|
|
py = round((side - ph) / 2, 2)
|
|
rx = round(side * radius_ratio, 2)
|
|
return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{side}" height="{side}" viewBox="0 0 {side} {side}" role="img" aria-label="Provenance icon">
|
|
<title>Provenance icon</title>
|
|
<rect x="0" y="0" width="{side}" height="{side}" rx="{rx}" fill="{BRONZE}"/>
|
|
<g transform="translate({px} {py})"><path d="{pd}" fill="{PAPER}"/></g>
|
|
</svg>
|
|
'''
|
|
|
|
open(f"{OUT}/provenance-icon.svg", "w").write(monogram(512))
|
|
open(f"{OUT}/favicon.svg", "w").write(monogram(48, radius_ratio=0.18))
|
|
|
|
print("WROTE:")
|
|
for f in sorted(os.listdir(OUT)):
|
|
p = os.path.join(OUT, f)
|
|
print(f" {f} ({os.path.getsize(p)} bytes)")
|
|
print(f"\nlogo lockup: {W} x {H}")
|
|
print(f"wordmark path size: w={wm_w} h={wm_h}")
|
|
print(f"tagline path size: w={tag_w} h={tag_h}")
|