mirror of
https://github.com/hanzoai/logo.git
synced 2026-09-30 22:52:39 +00:00
The wordmark is per-product — "Hanzo AI" on hanzo.ai, "Hanzo ID" on hanzo.id — so it is a table, not one asset. Sixteen names ship: the Hanzo products plus Lux, Zoo and Pars. They are PATHS. @hanzo/brand's wordmark.svg sets the name as an un-outlined <text> in Zen, so it renders in whatever fallback is around wherever Zen has not loaded — an OG card, a favicon, a PDF, a machine without the webfont. An outline needs no font at all. Cut from Zen at wght 606 (the house `medium` preset) and shaped through HarfBuzz, so pair spacing is the font's own GPOS kerning rather than naive advances — the first cut, with advances only, spaced visibly wrong. getWordmarkSVG(name) returns null for a name with no outline rather than falling back to <text>: a silent fallback is how a page ends up setting the brand in Arial. Add the name to scripts/wordmarks.py and re-run it. The table is ~143 KB, so build.ts also emits one file per name and `./wordmark/*` exports them — a browser bundle takes the one it needs; the table is for Node, where OG images and PDFs are made. The generator is NOT wired into `build`. Outlines are static data, and making every build depend on Python and fontTools to reproduce bytes that never change is a cost with no payer. This also fills a hook that had nothing behind it: build.ts called an optional (marks as any).getWordmarkWhite?.() that logos.ts never defined.
62 lines
2.7 KiB
Python
62 lines
2.7 KiB
Python
"""Outline the wordmarks from Zen and regenerate src/wordmarks.ts.
|
|
|
|
uv venv .venv && uv pip install --python .venv/bin/python "fonttools[woff]" brotli uharfbuzz
|
|
.venv/bin/python scripts/wordmarks.py
|
|
|
|
Text becomes PATHS, so a rendered wordmark needs no font present. Cut at wght
|
|
606 (the house `medium` preset) and shaped through HarfBuzz, so pair spacing is
|
|
the font's own GPOS kerning rather than naive advances.
|
|
|
|
Deliberately NOT part of `pnpm build`: outlines are static data, and making
|
|
every build depend on Python and fontTools to reproduce bytes that never change
|
|
is a cost with no payer. Run it when NAMES changes or the face is recut.
|
|
"""
|
|
import sys, json
|
|
from fontTools.ttLib import TTFont
|
|
from fontTools.varLib import instancer
|
|
from fontTools.pens.svgPathPen import SVGPathPen
|
|
from fontTools.pens.transformPen import TransformPen
|
|
from fontTools.pens.boundsPen import BoundsPen
|
|
import uharfbuzz as hb
|
|
|
|
FONT = __import__("os").environ.get("ZEN_FONT", "../font/packages/zen/dist/fonts/zen-sans/Zen-Variable.woff2")
|
|
|
|
def outline(text, wght=606):
|
|
# a static cut at the wordmark weight — 606 is the house `medium` preset
|
|
var = TTFont(FONT)
|
|
inst = instancer.instantiateVariableFont(var, {'wght': wght}, inplace=False)
|
|
inst.flavor = None # harfbuzz cannot read woff2; TTFont keeps the flavour on save
|
|
buf_io = __import__('io').BytesIO(); inst.save(buf_io); raw = buf_io.getvalue()
|
|
|
|
face = hb.Face(raw); font = hb.Font(face)
|
|
buf = hb.Buffer(); buf.add_str(text); buf.guess_segment_properties()
|
|
hb.shape(font, buf) # GPOS kerning applied here
|
|
|
|
gs = inst.getGlyphSet()
|
|
order = inst.getGlyphOrder()
|
|
d, x = [], 0
|
|
for info, pos in zip(buf.glyph_infos, buf.glyph_positions):
|
|
name = order[info.codepoint]
|
|
pen = SVGPathPen(gs)
|
|
gs[name].draw(TransformPen(pen, (1, 0, 0, 1, x + pos.x_offset, pos.y_offset)))
|
|
seg = pen.getCommands()
|
|
if seg: d.append(seg)
|
|
x += pos.x_advance
|
|
path = ' '.join(d)
|
|
|
|
# tight ink box, so the caller sizes by what is drawn rather than by metrics
|
|
bp = BoundsPen(gs)
|
|
xx = 0
|
|
for info, pos in zip(buf.glyph_infos, buf.glyph_positions):
|
|
gs[order[info.codepoint]].draw(TransformPen(bp, (1, 0, 0, 1, xx + pos.x_offset, pos.y_offset)))
|
|
xx += pos.x_advance
|
|
x0, y0, x1, y1 = bp.bounds
|
|
return {'d': path, 'box': [x0, y0, x1 - x0, y1 - y0], 'advance': x, 'upem': inst['head'].unitsPerEm}
|
|
|
|
if __name__ == '__main__':
|
|
print(json.dumps(outline(sys.argv[1] if len(sys.argv) > 1 else 'Hanzo AI')))
|
|
|
|
NAMES = ['Hanzo', 'Hanzo AI', 'Hanzo ID', 'Hanzo Cloud', 'Hanzo Dev', 'Hanzo App',
|
|
'Hanzo Chat', 'Hanzo Base', 'Hanzo Industries', 'Hanzo Commerce',
|
|
'Hanzo Platform', 'Hanzo Docs', 'Hanzo Studio', 'Lux', 'Zoo', 'Pars']
|