Files
2026-10-23 06:12:49 -03:00

80 lines
4.0 KiB
Python

import math
import os
def squircle(size=64, n=5.0, steps=90):
r = size/2
pts=[]
for i in range(steps*4):
t = 2*math.pi*i/(steps*4)
c, s = math.cos(t), math.sin(t)
pts.append((r*math.copysign(abs(c)**(2/n), c)+r, r*math.copysign(abs(s)**(2/n), s)+r))
return "M"+" L".join(f"{x:.2f} {y:.2f}" for x,y in pts)+"Z"
def norm(v):
l=math.hypot(*v); return (v[0]/l, v[1]/l)
def lam(top=(19.2,10.0), bot=(44.8,54.0), foot=(21.0,54.0), join_t=0.50, w=6.8):
"""A lambda drawn the way a type designer builds one: two straight strokes of
one weight, the long one running top-left to bottom-right and the short one
leaving it part way down and landing at the bottom-left, every end cut
square to the baseline, and no part of one stroke poking out of the other."""
(tx,ty),(bx,by)=top,bot
# --- the long stroke: a parallelogram cut horizontally at both ends
d=norm((bx-tx,by-ty)); phi_h = w/abs(d[1]) # horizontal thickness
def long_x(y): return tx+(bx-tx)*(y-ty)/(by-ty)
long_poly=[(tx-phi_h/2,ty),(tx+phi_h/2,ty),(bx+phi_h/2,by),(bx-phi_h/2,by)]
# --- the short stroke starts on the long one's centre line
jy=ty+(by-ty)*join_t; jx=long_x(jy)
sd=norm((foot[0]-jx,foot[1]-jy)) # direction of the short stroke
sh = w/abs(sd[1]) # its horizontal thickness
nrm=(-sd[1], sd[0]) # unit normal
# the two edges of the short stroke, as lines through (jx,jy)+-nrm*w/2
def edge_pt(sign, y):
ox,oy=jx+sign*nrm[0]*w/2, jy+sign*nrm[1]*w/2
t=(y-oy)/sd[1]; return (ox+sd[0]*t, y)
# its top end follows the long stroke's own direction, through the join point,
# so it ends inside the long stroke instead of sticking out of it
def on_long_dir(sign):
ox,oy=jx+sign*nrm[0]*w/2, jy+sign*nrm[1]*w/2
# intersect edge line (o + sd*t) with the line (j + d*s)
a=sd[0]*(-d[1]) - sd[1]*(-d[0]);
t=((jx-ox)*(-d[1]) - (jy-oy)*(-d[0]))/a
return (ox+sd[0]*t, oy+sd[1]*t)
p1,p2=on_long_dir(+1),on_long_dir(-1)
q1,q2=edge_pt(+1,foot[1]),edge_pt(-1,foot[1])
short_poly=[p1,p2,q2,q1]
return long_poly, short_poly
def path(*polys):
return " ".join("M"+" L".join(f"{x:.2f} {y:.2f}" for x,y in p)+"Z" for p in polys)
BLUE="#1B2BD0"
def svg(bg=BLUE, fg="#FFFFFF", size=64):
l,s=lam()
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="{size}" height="{size}">'
f'<path d="{squircle()}" fill="{bg}"/><path d="{path(l,s)}" fill="{fg}"/></svg>')
# ---------------------------------------------------------------------------
# Writes services/webmail/src/app/icon.svg and the SVGs the PNG step renders.
# Run: python3 scripts/brand/mark.py then node scripts/brand/icons.mjs
# ---------------------------------------------------------------------------
HERE = os.path.dirname(os.path.abspath(__file__))
WEBMAIL = os.path.join(HERE, "..", "..", "services", "webmail")
def write_all():
l, s = lam()
glyph = path(l, s)
shift = 'transform="translate(-0.9 0)"' # centres the glyph, not its bounding box
tile = f'<path d="{squircle()}" fill="{BLUE}"/>'
head = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">'
icon = f'{head}\n <!-- a lambda built as two square-cut strokes, on a tile with continuous corners -->\n {tile}\n <path {shift} d="{glyph}" fill="#fff"/>\n</svg>\n'
full = f'{head}<rect width="64" height="64" fill="{BLUE}"/><path {shift} d="{glyph}" fill="#fff"/></svg>'
safe = f'{head}<rect width="64" height="64" fill="{BLUE}"/><g transform="translate(32 32) scale(0.78) translate(-32 -32)"><path {shift} d="{glyph}" fill="#fff"/></g></svg>'
open(os.path.join(WEBMAIL, "src", "app", "icon.svg"), "w").write(icon)
for name, text in (("logo.svg", icon), ("logo-full.svg", full), ("logo-safe.svg", safe)):
open(os.path.join(HERE, name), "w").write(text)
print("glyph:", glyph)
if __name__ == "__main__":
write_all()