feat: outlined wordmarks, one per product
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.
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" aria-label="Lux" viewBox="79.64 -710 1633.68 722" xmlns="http://www.w3.org/2000/svg"><g transform="scale(1,-1)"><path d="M79.640380859375 0.0V710.0H210.958984375V40.4119873046875L139.5318603515625 114.0750732421875H547.007568359375V0.0Z M799.76025390625 -12.0Q718.288330078125 -12.0 671.2003173828125 41.5Q624.1123046875 95.0 624.1123046875 190.2359619140625V534.119873046875H753.430908203125V221.8876953125Q753.430908203125 155.1873779296875 775.3690795898438 124.043212890625Q797.3072509765625 92.8990478515625 843.18359375 92.8990478515625Q894.64794921875 92.8990478515625 923.232177734375 127.21923828125Q951.81640625 161.5394287109375 951.81640625 227.18359375V534.119873046875H1081.135009765625V0.0H962.408203125L959.5242919921875 147.0675048828125L976.232177734375 142.12744140625Q963.528076171875 66.415771484375 918.528076171875 27.2078857421875Q873.528076171875 -12.0 799.76025390625 -12.0Z M1176.176025390625 0.0 1368.2921142578125 272.0 1182.880126953125 534.119873046875H1322.90283203125L1445.0113525390625 353.0712890625L1563.94384765625 534.119873046875H1706.79052734375L1522.5545654296875 270.823974609375L1713.318603515625 0.0H1573.2958984375L1446.6593017578125 191.752685546875L1319.022705078125 0.0Z" fill="currentColor"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg role="img" aria-label="Zoo" viewBox="27.18 -710 1675.67 722" xmlns="http://www.w3.org/2000/svg"><g transform="scale(1,-1)"><path d="M27.176025390625 0.0V112.71923828125L378.7489013671875 595.9249267578125H42.2359619140625V710.0H538.958984375V597.28076171875L183.2662353515625 114.0750732421875H551.0189208984375V0.0Z M846.2435302734375 -12.0Q767.063720703125 -12.0 707.7378540039062 22.4119873046875Q648.4119873046875 56.823974609375 615.7340698242188 119.7359619140625Q583.05615234375 182.64794921875 583.05615234375 267.0599365234375Q583.05615234375 352.0599365234375 615.7340698242188 414.471923828125Q648.4119873046875 476.8839111328125 707.7378540039062 511.50189208984375Q767.063720703125 546.119873046875 846.2435302734375 546.119873046875Q925.42333984375 546.119873046875 984.4552001953125 511.50189208984375Q1043.487060546875 476.8839111328125 1076.1649780273438 414.471923828125Q1108.8428955078125 352.0599365234375 1108.8428955078125 267.0599365234375Q1108.8428955078125 182.64794921875 1076.1649780273438 119.7359619140625Q1043.487060546875 56.823974609375 984.4552001953125 22.4119873046875Q925.42333984375 -12.0 846.2435302734375 -12.0ZM846.2435302734375 92.3072509765625Q907.7078857421875 92.3072509765625 941.4100952148438 138.42144775390625Q975.1123046875 184.53564453125 975.1123046875 267.0599365234375Q975.1123046875 349.1722412109375 941.4100952148438 395.492431640625Q907.7078857421875 441.8126220703125 846.2435302734375 441.8126220703125Q784.7791748046875 441.8126220703125 750.782958984375 395.492431640625Q716.7867431640625 349.1722412109375 716.7867431640625 267.0599365234375Q716.7867431640625 184.53564453125 750.782958984375 138.42144775390625Q784.7791748046875 92.3072509765625 846.2435302734375 92.3072509765625Z M1440.2435302734375 -12.0Q1361.063720703125 -12.0 1301.7378540039062 22.4119873046875Q1242.4119873046875 56.823974609375 1209.7340698242188 119.7359619140625Q1177.05615234375 182.64794921875 1177.05615234375 267.0599365234375Q1177.05615234375 352.0599365234375 1209.7340698242188 414.471923828125Q1242.4119873046875 476.8839111328125 1301.7378540039062 511.50189208984375Q1361.063720703125 546.119873046875 1440.2435302734375 546.119873046875Q1519.42333984375 546.119873046875 1578.4552001953125 511.50189208984375Q1637.487060546875 476.8839111328125 1670.1649780273438 414.471923828125Q1702.8428955078125 352.0599365234375 1702.8428955078125 267.0599365234375Q1702.8428955078125 182.64794921875 1670.1649780273438 119.7359619140625Q1637.487060546875 56.823974609375 1578.4552001953125 22.4119873046875Q1519.42333984375 -12.0 1440.2435302734375 -12.0ZM1440.2435302734375 92.3072509765625Q1501.7078857421875 92.3072509765625 1535.4100952148438 138.42144775390625Q1569.1123046875 184.53564453125 1569.1123046875 267.0599365234375Q1569.1123046875 349.1722412109375 1535.4100952148438 395.492431640625Q1501.7078857421875 441.8126220703125 1440.2435302734375 441.8126220703125Q1378.7791748046875 441.8126220703125 1344.782958984375 395.492431640625Q1310.7867431640625 349.1722412109375 1310.7867431640625 267.0599365234375Q1310.7867431640625 184.53564453125 1344.782958984375 138.42144775390625Q1378.7791748046875 92.3072509765625 1440.2435302734375 92.3072509765625Z" fill="currentColor"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hanzo/logo",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.24",
|
||||
"packageManager": "[email protected]",
|
||||
"description": "Official Hanzo logo package with React components and utilities",
|
||||
"type": "module",
|
||||
@@ -42,7 +42,13 @@
|
||||
"./logo-white.svg": "./dist/logo-white.svg",
|
||||
"./svg/*": "./svg/*",
|
||||
"./dist/*": "./dist/*",
|
||||
"./logo-animated.svg": "./dist/logo-animated.svg"
|
||||
"./logo-animated.svg": "./dist/logo-animated.svg",
|
||||
"./wordmarks": {
|
||||
"types": "./dist/wordmarks.d.ts",
|
||||
"import": "./dist/wordmarks.js",
|
||||
"require": "./dist/wordmarks.js"
|
||||
},
|
||||
"./wordmark/*": "./dist/wordmarks/*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm clean && pnpm compile && pnpm build:cli && pnpm generate",
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""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']
|
||||
@@ -14,6 +14,7 @@ import { fileURLToPath } from 'url';
|
||||
import sharp from 'sharp';
|
||||
import { getColorSVG, getMonoSVG, getMenuBarSVG, getFaviconSVG, getFaviconRasterSVG, getWhiteSVG, getLightSVG } from './logos.js';
|
||||
import { ANIMATED_SVG } from './animated.js';
|
||||
import { WORDMARK_NAMES, getWordmarkSVG } from './wordmarks.js';
|
||||
// Namespace view of the same module — lets the build pick up OPTIONAL marks
|
||||
// (e.g. a wordmark) that only some brands define, without a hard import.
|
||||
import * as marks from './logos.js';
|
||||
@@ -234,6 +235,12 @@ async function buildAll(): Promise<void> {
|
||||
// already drifted.
|
||||
fs.writeFileSync('svg/hanzo-mark-animated.svg', ANIMATED_SVG);
|
||||
fs.writeFileSync('dist/logo-animated.svg', ANIMATED_SVG);
|
||||
// One file per wordmark: the table is ~143 KB, and a page needs one name.
|
||||
fs.mkdirSync('dist/wordmarks', { recursive: true });
|
||||
for (const name of WORDMARK_NAMES) {
|
||||
const svg = getWordmarkSVG(name);
|
||||
if (svg) fs.writeFileSync(`dist/wordmarks/${name.toLowerCase().replace(/ /g, '-')}.svg`, svg);
|
||||
}
|
||||
fs.writeFileSync('dist/logo-menubar.svg', menuBarSVG);
|
||||
fs.writeFileSync('dist/favicon.svg', faviconSVG);
|
||||
console.log('✓ Generated 6 SVG sources\n');
|
||||
|
||||
@@ -7,4 +7,5 @@
|
||||
export * from './logos.js';
|
||||
export * from './motion.js';
|
||||
export * from './types.js';
|
||||
export * from './react.js';
|
||||
export * from './react.js';
|
||||
export * from './wordmarks.js';
|
||||
|
||||