assets: add GoSentry wordmark logo (clock-dial variant)
Recommended variant from gosentry-logo.html: Space Grotesk SemiBold with an amber "G", a clock-dial "o" (ring + hands), and petrol "Sentry". Ships self-contained SVGs (glyphs outlined to paths, no font dependency) in light/dark/mono plus transparent PNGs at 256-2048px, with a README and the gen_logo.py/raster.py generators for reproducibility. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate GoSentry wordmark logo assets (recommended variant:
|
||||
Space Grotesk SemiBold + amber 'G' + clock-dial 'o' with hands + 'Sentry')."""
|
||||
import os
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.pens.svgPathPen import SVGPathPen
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
from fontTools.pens.boundsPen import BoundsPen
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(HERE, "out")
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
AMBER = "#F7A80C"
|
||||
PETROL = "#0A4A58"
|
||||
WHITE = "#FFFFFF"
|
||||
|
||||
LS = -30 # letter-spacing -0.03em at 1000 upm
|
||||
|
||||
f = TTFont(os.path.join(HERE, "SpaceGrotesk-600.ttf"))
|
||||
cmap = f.getBestCmap()
|
||||
hmtx = f["hmtx"]
|
||||
gs = f.getGlyphSet()
|
||||
|
||||
def glyph_path(ch, dx):
|
||||
"""Return SVG path 'd' for ch, shifted by dx in font units (y still up)."""
|
||||
g = cmap[ord(ch)]
|
||||
pen = SVGPathPen(gs)
|
||||
tpen = TransformPen(pen, (1, 0, 0, 1, dx, 0))
|
||||
gs[g].draw(tpen)
|
||||
return pen.getCommands(), hmtx[g][0]
|
||||
|
||||
def o_metrics():
|
||||
g = cmap[ord("o")]
|
||||
bp = BoundsPen(gs); gs[g].draw(bp)
|
||||
xmin,ymin,xmax,ymax = bp.bounds
|
||||
return hmtx[g][0], xmin, ymin, xmax, ymax
|
||||
|
||||
# ---- layout the wordmark ---------------------------------------------------
|
||||
x = 0.0
|
||||
G_d, adv = glyph_path("G", x); x += adv + LS
|
||||
|
||||
# dial occupies the 'o' advance slot
|
||||
o_adv, oxmin, oymin, oxmax, oymax = o_metrics()
|
||||
o_left = x
|
||||
cx = o_left + (oxmin + oxmax) / 2.0
|
||||
cy = (oymin + oymax) / 2.0
|
||||
R = ((oxmax - oxmin) + (oymax - oymin)) / 4.0 # avg radius, matches the 'o'
|
||||
x += o_adv + LS
|
||||
|
||||
sentry_d = []
|
||||
for ch in "Sentry":
|
||||
d, adv = glyph_path(ch, x)
|
||||
sentry_d.append(d)
|
||||
x += adv + LS
|
||||
x -= LS # no trailing letter-spacing
|
||||
SENTRY_D = " ".join(sentry_d)
|
||||
|
||||
# ---- dial geometry (matched to the 'o', proportions from the HTML mock) ----
|
||||
SW = R * 0.30 # ring stroke width
|
||||
Rmid = R - SW / 2.0 # centreline radius of ring
|
||||
HW = R * 0.21 # hand width
|
||||
hour_len = R * 0.54 # 12 o'clock hand
|
||||
min_len = R * 0.46 # ~4 o'clock hand
|
||||
min_ang = 62 # degrees clockwise from 12
|
||||
import math
|
||||
mx = cx + min_len * math.sin(math.radians(min_ang))
|
||||
my = cy + min_len * math.cos(math.radians(min_ang)) # font-up: +y is up
|
||||
cap = R * 0.14
|
||||
|
||||
dial = f'''<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{Rmid:.1f}" fill="none" stroke="{AMBER}" stroke-width="{SW:.1f}"/>
|
||||
<path d="M{cx:.1f} {cy:.1f} L{cx:.1f} {cy+hour_len:.1f}" stroke="{AMBER}" stroke-width="{HW:.1f}" stroke-linecap="round"/>
|
||||
<path d="M{cx:.1f} {cy:.1f} L{mx:.1f} {my:.1f}" stroke="{AMBER}" stroke-width="{HW:.1f}" stroke-linecap="round"/>
|
||||
<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{cap:.1f}" fill="{AMBER}"/>'''
|
||||
|
||||
# ---- overall bounds (font units, y up) -------------------------------------
|
||||
bp = BoundsPen(gs)
|
||||
xall = 0.0
|
||||
gG = cmap[ord("G")]; gs[gG].draw(TransformPen(bp,(1,0,0,1,0,0)))
|
||||
xall += hmtx[gG][0] + LS + o_adv + LS
|
||||
for ch in "Sentry":
|
||||
g = cmap[ord(ch)]
|
||||
gs[g].draw(TransformPen(bp,(1,0,0,1,xall,0)))
|
||||
xall += hmtx[g][0] + LS
|
||||
bx0,by0,bx1,by1 = bp.bounds
|
||||
# include the dial extents
|
||||
bx0 = min(bx0, cx-R-SW/2); bx1 = max(bx1, cx+R+SW/2)
|
||||
by0 = min(by0, cy-R-SW/2); by1 = max(by1, cy+R+SW/2)
|
||||
|
||||
PAD = 60
|
||||
W = (bx1 - bx0) + 2*PAD
|
||||
H = (by1 - by0) + 2*PAD
|
||||
# transform: font(x,y up) -> screen: translate then flip y
|
||||
tx = PAD - bx0
|
||||
ty = PAD + by1
|
||||
transform = f"matrix(1 0 0 -1 {tx:.2f} {ty:.2f})"
|
||||
|
||||
def svg(sentry_color, bg=None, name=""):
|
||||
bgrect = f'<rect width="{W:.1f}" height="{H:.1f}" fill="{bg}"/>\n' if bg else ""
|
||||
return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W:.1f} {H:.1f}" role="img" aria-label="GoSentry">
|
||||
{bgrect}<g transform="{transform}">
|
||||
<path d="{G_d}" fill="{AMBER}"/>
|
||||
<path d="{SENTRY_D}" fill="{sentry_color}"/>
|
||||
{dial}
|
||||
</g>
|
||||
</svg>
|
||||
'''
|
||||
|
||||
variants = {
|
||||
"gosentry-logo.svg": svg(PETROL), # light bg, transparent
|
||||
"gosentry-logo-dark.svg": svg(WHITE), # dark bg, transparent
|
||||
"gosentry-logo-onlight.svg": svg(PETROL, bg="#FFFFFF"),
|
||||
"gosentry-logo-ondark.svg": svg(WHITE, bg="#04262E"),
|
||||
"gosentry-logo-mono.svg": svg(PETROL).replace(AMBER, PETROL), # single-colour petrol
|
||||
}
|
||||
for fn, data in variants.items():
|
||||
with open(os.path.join(OUT, fn), "w", encoding="utf-8") as fh:
|
||||
fh.write(data)
|
||||
print("wrote", fn)
|
||||
print("viewBox %.1f x %.1f" % (W, H))
|
||||
Reference in New Issue
Block a user