diff --git a/assets/logo/README.md b/assets/logo/README.md new file mode 100644 index 0000000..5401556 --- /dev/null +++ b/assets/logo/README.md @@ -0,0 +1,41 @@ +# GoSentry logo + +Recommended wordmark variant from [`../gosentry-logo.html`](../gosentry-logo.html): +**Space Grotesk SemiBold** with an amber **G**, a **clock-dial "o"** (ring + hands), +and a petrol **"Sentry"**. The dial sits exactly in the `o` slot, so the schedule / +"watch" theme lives inside the letters instead of a bolt-on icon. + +## Colors + +| token | hex | use | +|--------|-----------|-----------------------------| +| amber | `#F7A80C` | `G`, dial ring + hands | +| petrol | `#0A4A58` | `Sentry` (light background) | +| white | `#FFFFFF` | `Sentry` (dark background) | + +## Files + +Vector (self-contained — glyphs are outlined to paths, no font required): + +- `gosentry-logo.svg` — transparent, petrol `Sentry` (for light backgrounds) +- `gosentry-logo-dark.svg` — transparent, white `Sentry` (for dark backgrounds) +- `gosentry-logo-mono.svg` — single-color petrol + +Raster (transparent PNG, aspect ≈ 4326×1034 ≈ 4.18:1): + +- `gosentry-logo-{256,512,1024,2048}.png` — petrol `Sentry` +- `gosentry-logo-dark-{256,512,1024,2048}.png` — white `Sentry` + +## Regenerating + +Requires `fonttools` and `matplotlib`, plus the Space Grotesk variable font +(SIL OFL) instanced to weight 600 as `SpaceGrotesk-600.ttf`: + +```sh +python gen_logo.py # writes SVGs into ./out +python raster.py # writes PNGs into ./out +``` + +`gen_logo.py` (SVG) and `raster.py` (PNG) share the same layout + dial geometry, +so both outputs stay identical. Space Grotesk is licensed under the SIL Open Font +License; outlining its glyphs into a logo is permitted. diff --git a/assets/logo/gen_logo.py b/assets/logo/gen_logo.py new file mode 100644 index 0000000..1ad66e9 --- /dev/null +++ b/assets/logo/gen_logo.py @@ -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''' + + + ''' + +# ---- 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'\n' if bg else "" + return f''' +{bgrect} + + + {dial} + + +''' + +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)) diff --git a/assets/logo/gosentry-logo-1024.png b/assets/logo/gosentry-logo-1024.png new file mode 100644 index 0000000..e9d699c Binary files /dev/null and b/assets/logo/gosentry-logo-1024.png differ diff --git a/assets/logo/gosentry-logo-2048.png b/assets/logo/gosentry-logo-2048.png new file mode 100644 index 0000000..54a40bb Binary files /dev/null and b/assets/logo/gosentry-logo-2048.png differ diff --git a/assets/logo/gosentry-logo-256.png b/assets/logo/gosentry-logo-256.png new file mode 100644 index 0000000..689ba42 Binary files /dev/null and b/assets/logo/gosentry-logo-256.png differ diff --git a/assets/logo/gosentry-logo-512.png b/assets/logo/gosentry-logo-512.png new file mode 100644 index 0000000..7feff60 Binary files /dev/null and b/assets/logo/gosentry-logo-512.png differ diff --git a/assets/logo/gosentry-logo-dark-1024.png b/assets/logo/gosentry-logo-dark-1024.png new file mode 100644 index 0000000..b326057 Binary files /dev/null and b/assets/logo/gosentry-logo-dark-1024.png differ diff --git a/assets/logo/gosentry-logo-dark-2048.png b/assets/logo/gosentry-logo-dark-2048.png new file mode 100644 index 0000000..527700b Binary files /dev/null and b/assets/logo/gosentry-logo-dark-2048.png differ diff --git a/assets/logo/gosentry-logo-dark-256.png b/assets/logo/gosentry-logo-dark-256.png new file mode 100644 index 0000000..b27a65a Binary files /dev/null and b/assets/logo/gosentry-logo-dark-256.png differ diff --git a/assets/logo/gosentry-logo-dark-512.png b/assets/logo/gosentry-logo-dark-512.png new file mode 100644 index 0000000..4b9f24e Binary files /dev/null and b/assets/logo/gosentry-logo-dark-512.png differ diff --git a/assets/logo/gosentry-logo-dark.svg b/assets/logo/gosentry-logo-dark.svg new file mode 100644 index 0000000..334a6ea --- /dev/null +++ b/assets/logo/gosentry-logo-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/logo/gosentry-logo-mono.svg b/assets/logo/gosentry-logo-mono.svg new file mode 100644 index 0000000..0daf8cb --- /dev/null +++ b/assets/logo/gosentry-logo-mono.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/logo/gosentry-logo.svg b/assets/logo/gosentry-logo.svg new file mode 100644 index 0000000..5a07ed9 --- /dev/null +++ b/assets/logo/gosentry-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/logo/raster.py b/assets/logo/raster.py new file mode 100644 index 0000000..174a5c2 --- /dev/null +++ b/assets/logo/raster.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Rasterize the GoSentry wordmark to PNG at several widths, reusing the +same font outlines + dial geometry as gen_logo.py (no SVG rasterizer needed).""" +import os, math +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from matplotlib.path import Path +from matplotlib.patches import PathPatch, Circle +from matplotlib.lines import Line2D +from fontTools.ttLib import TTFont +from fontTools.pens.basePen import BasePen +from fontTools.pens.boundsPen import BoundsPen +from fontTools.pens.transformPen import TransformPen + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT = os.path.join(HERE, "out"); os.makedirs(OUT, exist_ok=True) +AMBER, PETROL, WHITE = "#F7A80C", "#0A4A58", "#FFFFFF" +LS = -30 + +f = TTFont(os.path.join(HERE, "SpaceGrotesk-600.ttf")) +cmap, hmtx, gs = f.getBestCmap(), f["hmtx"], f.getGlyphSet() + +class MplPen(BasePen): + def __init__(self, glyphSet): + super().__init__(glyphSet); self.v=[]; self.c=[] + def _moveTo(self,p): self.v.append(p); self.c.append(Path.MOVETO) + def _lineTo(self,p): self.v.append(p); self.c.append(Path.LINETO) + def _curveToOne(self,p1,p2,p3): + self.v += [p1,p2,p3]; self.c += [Path.CURVE4]*3 + def _qCurveToOne(self,p1,p2): + self.v += [p1,p2]; self.c += [Path.CURVE3]*2 + def _closePath(self): + self.v.append((0,0)); self.c.append(Path.CLOSEPOLY) + +def glyph_mplpath(ch, dx): + pen = MplPen(gs) + gs[cmap[ord(ch)]].draw(TransformPen(pen,(1,0,0,1,dx,0))) + return Path(pen.v, pen.c), hmtx[cmap[ord(ch)]][0] + +# layout +x=0.0 +paths_amber=[]; paths_petrol=[] +p,adv = glyph_mplpath("G",x); paths_amber.append(p); x+=adv+LS +og=cmap[ord("o")]; bp=BoundsPen(gs); gs[og].draw(bp) +oxmin,oymin,oxmax,oymax=bp.bounds; o_adv=hmtx[og][0] +cx=x+(oxmin+oxmax)/2; cy=(oymin+oymax)/2 +R=((oxmax-oxmin)+(oymax-oymin))/4 +x+=o_adv+LS +for ch in "Sentry": + p,adv=glyph_mplpath(ch,x); paths_petrol.append(p); x+=adv+LS +x-=LS + +SW=R*0.30; Rmid=R-SW/2; HW=R*0.21 +hour_len=R*0.54; min_len=R*0.46; ang=math.radians(62) +mx=cx+min_len*math.sin(ang); my=cy+min_len*math.cos(ang); cap=R*0.14 + +# bounds +allb=BoundsPen(gs); gs[cmap[ord('G')]].draw(allb) +xx=hmtx[cmap[ord('G')]][0]+LS+o_adv+LS +for ch in "Sentry": + gs[cmap[ord(ch)]].draw(TransformPen(allb,(1,0,0,1,xx,0))); xx+=hmtx[cmap[ord(ch)]][0]+LS +bx0,by0,bx1,by1=allb.bounds +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 +X0,X1=bx0-PAD,bx1+PAD; Y0,Y1=by0-PAD,by1+PAD +W=X1-X0; H=Y1-Y0 + +def render(path_png, width_px, sentry_color, bg=None, mono=False): + dpi=100 + fw=width_px/dpi; fh=fw*H/W + fig=plt.figure(figsize=(fw,fh),dpi=dpi) + ax=fig.add_axes([0,0,1,1]); ax.set_xlim(X0,X1); ax.set_ylim(Y0,Y1) + ax.set_aspect('equal'); ax.axis('off') + if bg: fig.patch.set_facecolor(bg); ax.set_facecolor(bg) + else: fig.patch.set_alpha(0) + amberc = sentry_color if mono else AMBER + for p in paths_amber: ax.add_patch(PathPatch(p,facecolor=amberc,edgecolor='none',antialiased=True)) + for p in paths_petrol: ax.add_patch(PathPatch(p,facecolor=sentry_color,edgecolor='none',antialiased=True)) + pt_per_unit = fw/W*72 + ax.add_patch(Circle((cx,cy),Rmid,fill=False,edgecolor=amberc,linewidth=SW*pt_per_unit)) + for (ex,ey) in [(cx,cy+hour_len),(mx,my)]: + ax.add_line(Line2D([cx,ex],[cy,ey],color=amberc,linewidth=HW*pt_per_unit, + solid_capstyle='round')) + ax.add_patch(Circle((cx,cy),cap,facecolor=amberc,edgecolor='none')) + fig.savefig(path_png,dpi=dpi,transparent=(bg is None)) + plt.close(fig) + print("wrote",os.path.basename(path_png)) + +for w in (256,512,1024,2048): + render(os.path.join(OUT,f"gosentry-logo-{w}.png"),w,PETROL) + render(os.path.join(OUT,f"gosentry-logo-dark-{w}.png"),w,WHITE) +render(os.path.join(OUT,"gosentry-logo-onlight-1024.png"),1024,PETROL,bg="#FFFFFF") +render(os.path.join(OUT,"gosentry-logo-ondark-1024.png"),1024,WHITE,bg="#04262E") +render(os.path.join(OUT,"gosentry-logo-mono-1024.png"),1024,PETROL,mono=True) +print(f"aspect {W:.0f}x{H:.0f}")