branding-engine 0.1.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +433 -73
- package/bin/cli.mjs +8 -5
- package/examples/severino-labs/README.md +95 -0
- package/examples/severino-labs/brand.json +30 -0
- package/examples/severino-labs/generated/cards/social-card.png +0 -0
- package/examples/severino-labs/generated/severino-labs/README.md +49 -0
- package/examples/severino-labs/generated/severino-labs/icons/apple-touch-icon.png +0 -0
- package/examples/severino-labs/generated/severino-labs/icons/favicon-192.png +0 -0
- package/examples/severino-labs/generated/severino-labs/icons/favicon-32.png +0 -0
- package/examples/severino-labs/generated/severino-labs/icons/favicon.ico +0 -0
- package/examples/severino-labs/generated/severino-labs/icons/favicon.svg +1 -0
- package/examples/severino-labs/generated/severino-labs/mark/mark-1024.png +0 -0
- package/examples/severino-labs/generated/severino-labs/mark/mark-512.png +0 -0
- package/examples/severino-labs/generated/severino-labs/mark/mark-transparent-dark.png +0 -0
- package/examples/severino-labs/generated/severino-labs/mark/mark-transparent-light.png +0 -0
- package/examples/severino-labs/generated/severino-labs/mark/mark.svg +1 -0
- package/examples/severino-labs/generated/severino-labs/sheet/overview.png +0 -0
- package/examples/severino-labs/generated/severino-labs/sheet/palette.png +0 -0
- package/examples/severino-labs/generated/severino-labs/sheet/sheet-mark.png +0 -0
- package/examples/severino-labs/generated/severino-labs/sheet/type-specimen.png +0 -0
- package/examples/severino-labs/generated/severino-labs/web/head.html +6 -0
- package/examples/severino-labs/generated/severino-labs/web/site.webmanifest +19 -0
- package/examples/severino-labs/generated/severino-labs/web/tokens.css +7 -0
- package/examples/severino-labs/generated/severino-labs/wordmark/wordmark-caps-dark.png +0 -0
- package/examples/severino-labs/generated/severino-labs/wordmark/wordmark-caps-light.png +0 -0
- package/examples/severino-labs/generated/severino-labs/wordmark/wordmark-caps.svg +1 -0
- package/examples/severino-labs/generated/severino-labs/wordmark/wordmark-dark.png +0 -0
- package/examples/severino-labs/generated/severino-labs/wordmark/wordmark-light.png +0 -0
- package/examples/severino-labs/generated/severino-labs/wordmark/wordmark.svg +1 -0
- package/examples/severino-labs/studio.jpg +0 -0
- package/index.mjs +1 -0
- package/package.json +13 -6
- package/src/build.mjs +8 -6
- package/src/lib/extract-glyphs.mjs +65 -0
- package/src/lib/glyphs.mjs +6 -19
- package/src/lib/identity.mjs +11 -0
- package/src/lib/mark.mjs +28 -8
- package/src/lib/render.mjs +11 -2
- package/src/lib/wordmark.mjs +6 -1
- package/src/make-mark.mjs +2 -0
- package/src/make-sheet.mjs +2 -0
- package/src/make-web.mjs +2 -0
- package/src/make-wordmark.mjs +3 -1
- package/src/site.mjs +3 -1
- package/requirements.txt +0 -3
- package/src/lib/extract-glyphs.py +0 -75
package/src/lib/mark.mjs
CHANGED
|
@@ -4,18 +4,29 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Outlines come from brand-glyphs.json (A-Z and 0-9 are bundled by default).
|
|
6
6
|
import { loadGlyphs } from './glyphs.mjs';
|
|
7
|
+
import { normalizeGlyph } from './identity.mjs';
|
|
7
8
|
|
|
8
9
|
// Read glyphs lazily so a BRAND_GLYPHS set in-process (by the build) is honored.
|
|
9
10
|
// Defaults to brand-glyphs.json; point at another extracted set with BRAND_GLYPHS.
|
|
10
11
|
let _glyphData;
|
|
12
|
+
let _glyphFile;
|
|
11
13
|
function glyphData() {
|
|
12
|
-
|
|
14
|
+
const file = process.env.BRAND_GLYPHS || 'brand-glyphs.json';
|
|
15
|
+
if (!_glyphData || _glyphFile !== file) {
|
|
16
|
+
_glyphData = loadGlyphs(file);
|
|
17
|
+
_glyphFile = file;
|
|
18
|
+
}
|
|
13
19
|
return _glyphData;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
const RENDER = {
|
|
17
23
|
letterSpacing: -0.045, // em
|
|
18
|
-
widthRatio:
|
|
24
|
+
widthRatio: {
|
|
25
|
+
1: 0.46,
|
|
26
|
+
2: 0.63,
|
|
27
|
+
3: 0.76,
|
|
28
|
+
},
|
|
29
|
+
heightRatio: 0.56, // cap narrow glyphs such as "I" by height
|
|
19
30
|
radiusRatio: 0.22, // rounded-square corner radius (0 = square)
|
|
20
31
|
};
|
|
21
32
|
|
|
@@ -31,8 +42,7 @@ function layout(glyph) {
|
|
|
31
42
|
if (!g) {
|
|
32
43
|
throw new Error(
|
|
33
44
|
`No outline for glyph "${ch}". Available: ${Object.keys(glyphs).join(', ')}. ` +
|
|
34
|
-
`
|
|
35
|
-
`font, ensure python3 + fonttools are installed.`,
|
|
45
|
+
`Re-run through buildKit or buildBrand so the Node extractor can cache it.`,
|
|
36
46
|
);
|
|
37
47
|
}
|
|
38
48
|
placed.push({ x: penX, g });
|
|
@@ -45,7 +55,14 @@ function layout(glyph) {
|
|
|
45
55
|
yMin = Math.min(yMin, g.bounds.yMin);
|
|
46
56
|
yMax = Math.max(yMax, g.bounds.yMax);
|
|
47
57
|
}
|
|
48
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
placed,
|
|
60
|
+
count: chars.length,
|
|
61
|
+
cx: (xMin + xMax) / 2,
|
|
62
|
+
cy: (yMin + yMax) / 2,
|
|
63
|
+
gw: xMax - xMin,
|
|
64
|
+
gh: yMax - yMin,
|
|
65
|
+
};
|
|
49
66
|
}
|
|
50
67
|
|
|
51
68
|
/**
|
|
@@ -55,11 +72,14 @@ function layout(glyph) {
|
|
|
55
72
|
* @param {boolean} [opts.rounded=true] rounded-square (false = full square)
|
|
56
73
|
* @param {string|null} [opts.bg] tile fill, or null for transparent
|
|
57
74
|
* @param {string} [opts.fg='#ffffff'] glyph fill
|
|
58
|
-
* @param {string} [opts.glyph='JS']
|
|
75
|
+
* @param {string} [opts.glyph='JS'] 1-3 alphanumeric mark characters
|
|
59
76
|
*/
|
|
60
77
|
export function markSvg({ size = 512, rounded = true, bg, fg = '#ffffff', glyph = 'JS' } = {}) {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
78
|
+
const normalizedGlyph = normalizeGlyph(glyph);
|
|
79
|
+
const { placed, count, cx, cy, gw, gh } = layout(normalizedGlyph);
|
|
80
|
+
const widthScale = (RENDER.widthRatio[count] * size) / gw;
|
|
81
|
+
const heightScale = (RENDER.heightRatio * size) / gh;
|
|
82
|
+
const s = Math.min(widthScale, heightScale);
|
|
63
83
|
const rx = rounded ? +(size * RENDER.radiusRatio).toFixed(2) : 0;
|
|
64
84
|
const bgRect = bg ? `<rect width="${size}" height="${size}" rx="${rx}" fill="${bg}"/>` : '';
|
|
65
85
|
const paths = placed
|
package/src/lib/render.mjs
CHANGED
|
@@ -8,9 +8,18 @@ import { fontPath } from './font.mjs';
|
|
|
8
8
|
|
|
9
9
|
// The bundled font embedded as a data URI, as an @font-face for `family`.
|
|
10
10
|
export function fontFaceCss(family) {
|
|
11
|
-
const
|
|
11
|
+
const font = fontPath();
|
|
12
|
+
const ext = font.split('.').pop().toLowerCase();
|
|
13
|
+
const formats = {
|
|
14
|
+
otf: ['font/otf', 'opentype'],
|
|
15
|
+
ttf: ['font/ttf', 'truetype'],
|
|
16
|
+
woff: ['font/woff', 'woff'],
|
|
17
|
+
woff2: ['font/woff2', 'woff2'],
|
|
18
|
+
};
|
|
19
|
+
const [mime, format] = formats[ext] || ['application/octet-stream', ext];
|
|
20
|
+
const b64 = readFileSync(font).toString('base64');
|
|
12
21
|
return `@font-face{font-family:${family};font-weight:200 900;font-display:block;` +
|
|
13
|
-
`src:url(data
|
|
22
|
+
`src:url(data:${mime};base64,${b64}) format('${format}')}`;
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
// Playwright is an optional dependency: only the sheet and social-card renderers
|
package/src/lib/wordmark.mjs
CHANGED
|
@@ -21,8 +21,13 @@ const LETTER_SPACING = { title: -0.03125, caps: -0.02 }; // em
|
|
|
21
21
|
// Read glyphs lazily so a cache (re)written earlier in the same process, or a
|
|
22
22
|
// BRAND_WORDMARK_GLYPHS set by build.mjs, is honored on first render.
|
|
23
23
|
let _glyphData;
|
|
24
|
+
let _glyphFile;
|
|
24
25
|
function glyphData() {
|
|
25
|
-
|
|
26
|
+
const file = wordmarkGlyphFile();
|
|
27
|
+
if (!_glyphData || _glyphFile !== file) {
|
|
28
|
+
_glyphData = loadGlyphs(file);
|
|
29
|
+
_glyphFile = file;
|
|
30
|
+
}
|
|
26
31
|
return _glyphData;
|
|
27
32
|
}
|
|
28
33
|
|
package/src/make-mark.mjs
CHANGED
|
@@ -8,10 +8,12 @@ import path from 'node:path';
|
|
|
8
8
|
import sharp from 'sharp';
|
|
9
9
|
import { markSvg } from './lib/mark.mjs';
|
|
10
10
|
import { normalizeHex } from './lib/color.mjs';
|
|
11
|
+
import { normalizeGlyph } from './lib/identity.mjs';
|
|
11
12
|
import { pngsToIco } from './lib/ico.mjs';
|
|
12
13
|
|
|
13
14
|
export async function makeMark({ slug, hex, glyph = 'JS', outDir }) {
|
|
14
15
|
const fill = normalizeHex(hex);
|
|
16
|
+
glyph = normalizeGlyph(glyph);
|
|
15
17
|
const iconsDir = path.join(outDir, slug, 'icons');
|
|
16
18
|
const markDir = path.join(outDir, slug, 'mark');
|
|
17
19
|
fs.mkdirSync(iconsDir, { recursive: true });
|
package/src/make-sheet.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import sharp from 'sharp';
|
|
9
9
|
import { markSvg } from './lib/mark.mjs';
|
|
10
|
+
import { normalizeGlyph } from './lib/identity.mjs';
|
|
10
11
|
import { fontPath } from './lib/font.mjs';
|
|
11
12
|
import { darken, normalizeHex } from './lib/color.mjs';
|
|
12
13
|
import { esc } from './lib/html.mjs';
|
|
@@ -21,6 +22,7 @@ function prettyFont(file) {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export async function makeSheet({ slug, hex, glyph = 'JS', wordmark, deep, browser, outDir }) {
|
|
25
|
+
glyph = normalizeGlyph(glyph);
|
|
24
26
|
const fill = normalizeHex(hex);
|
|
25
27
|
const deepShade = deep ? normalizeHex(deep) : darken(fill);
|
|
26
28
|
const title = wordmark || glyph;
|
package/src/make-web.mjs
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { darken, normalizeHex } from './lib/color.mjs';
|
|
8
|
+
import { normalizeGlyph } from './lib/identity.mjs';
|
|
8
9
|
|
|
9
10
|
export function makeWeb({ slug, hex, glyph = 'JS', name, deep, onColor, outDir }) {
|
|
10
11
|
const accent = normalizeHex(hex);
|
|
12
|
+
glyph = normalizeGlyph(glyph);
|
|
11
13
|
const deepShade = deep ? normalizeHex(deep) : darken(accent);
|
|
12
14
|
const onAccent = onColor ? normalizeHex(onColor) : '#ffffff';
|
|
13
15
|
const label = name || slug;
|
package/src/make-wordmark.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import sharp from 'sharp';
|
|
9
9
|
import { wordmarkSvg } from './lib/wordmark.mjs';
|
|
10
10
|
import { WORDMARK_CHARS, ensureGlyphs, wordmarkGlyphFile } from './lib/glyphs.mjs';
|
|
11
|
+
import { normalizeGlyph } from './lib/identity.mjs';
|
|
11
12
|
|
|
12
13
|
const INK = { light: '#0b0620', dark: '#ffffff' }; // ink for light / dark backgrounds
|
|
13
14
|
const PNG_HEIGHT = 512; // raster export height; SVG stays the source of truth
|
|
@@ -17,13 +18,14 @@ const VARIANTS = [
|
|
|
17
18
|
];
|
|
18
19
|
|
|
19
20
|
export async function makeWordmark({ slug, hex, text, glyph = 'JS', weight = 700, outDir }) {
|
|
21
|
+
glyph = normalizeGlyph(glyph);
|
|
20
22
|
const dir = path.join(outDir, slug, 'wordmark');
|
|
21
23
|
mkdirSync(dir, { recursive: true });
|
|
22
24
|
|
|
23
25
|
// The build pre-extracts, but a one-off kit lands straight here, so make sure
|
|
24
26
|
// the cache exists. It bundles the whole alphabet (WORDMARK_CHARS) so a one-off
|
|
25
27
|
// never overwrites the shared set with just its own name's letters.
|
|
26
|
-
ensureGlyphs({ file: wordmarkGlyphFile(), weight, chars: WORDMARK_CHARS, label: 'wordmark glyphs' });
|
|
28
|
+
await ensureGlyphs({ file: wordmarkGlyphFile(), weight, chars: WORDMARK_CHARS, label: 'wordmark glyphs' });
|
|
27
29
|
|
|
28
30
|
const toPng = (svg) => sharp(Buffer.from(svg)).resize({ height: PNG_HEIGHT }).png().toBuffer();
|
|
29
31
|
for (const { caps, base } of VARIANTS) {
|
package/src/site.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import path from 'node:path';
|
|
|
11
11
|
import sharp from 'sharp';
|
|
12
12
|
import { markSvg } from './lib/mark.mjs';
|
|
13
13
|
import { darken, normalizeHex } from './lib/color.mjs';
|
|
14
|
+
import { normalizeGlyph } from './lib/identity.mjs';
|
|
14
15
|
import { pngsToIco } from './lib/ico.mjs';
|
|
15
16
|
|
|
16
17
|
const DEFAULT_CONFIG = {
|
|
@@ -27,6 +28,7 @@ function headSnippet(accent) {
|
|
|
27
28
|
'<link rel="icon" type="image/svg+xml" href="/favicon.svg" />',
|
|
28
29
|
'<link rel="apple-touch-icon" href="/apple-touch-icon.png" />',
|
|
29
30
|
'<link rel="manifest" href="/site.webmanifest" />',
|
|
31
|
+
'<link rel="stylesheet" href="/brand-tokens.css" />',
|
|
30
32
|
`<meta name="theme-color" content="${accent}" />`,
|
|
31
33
|
].join('\n');
|
|
32
34
|
}
|
|
@@ -69,7 +71,7 @@ export async function generateSite({ config, publicDir = 'public', cwd = process
|
|
|
69
71
|
const accent = normalizeHex(cfg.accent);
|
|
70
72
|
const onAccent = cfg.onColor ? normalizeHex(cfg.onColor) : '#ffffff';
|
|
71
73
|
const deep = cfg.deep ? normalizeHex(cfg.deep) : darken(accent);
|
|
72
|
-
const glyph = cfg.glyph || 'JS';
|
|
74
|
+
const glyph = normalizeGlyph(cfg.glyph || 'JS');
|
|
73
75
|
const name = cfg.name || 'Site';
|
|
74
76
|
|
|
75
77
|
const pub = path.resolve(cwd, publicDir);
|
package/requirements.txt
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Extract glyph outlines as SVG path data for the brand mark.
|
|
3
|
-
|
|
4
|
-
Defaults to the bundled Inter variable woff2, but any font works: pass a path to
|
|
5
|
-
a .ttf/.otf/.woff2. Variable fonts are instantiated at the given weight; static
|
|
6
|
-
fonts use their single master. Output is JSON the node generators consume, so
|
|
7
|
-
they stay pure-node.
|
|
8
|
-
|
|
9
|
-
Usage:
|
|
10
|
-
python3 bin/lib/extract-glyphs.py "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 800
|
|
11
|
-
python3 bin/lib/extract-glyphs.py "AB" 700 ~/fonts/SomeFont.ttf
|
|
12
|
-
python3 bin/lib/extract-glyphs.py "AB" 700 ~/fonts/SomeFont.ttf myfont-glyphs.json
|
|
13
|
-
|
|
14
|
-
The default run writes brand-glyphs.json (the kit's glyph cache). A custom font
|
|
15
|
-
writes <font-stem>-glyphs.json unless you name the output, so the brand cache is
|
|
16
|
-
never clobbered by accident. Point a generator at a non-default file with
|
|
17
|
-
BRAND_GLYPHS=<file> (e.g. BRAND_GLYPHS=somefont-glyphs.json ./kit.sh ...).
|
|
18
|
-
"""
|
|
19
|
-
import json
|
|
20
|
-
import sys
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
|
|
23
|
-
from fontTools.ttLib import TTFont
|
|
24
|
-
from fontTools.varLib.instancer import instantiateVariableFont
|
|
25
|
-
from fontTools.pens.svgPathPen import SVGPathPen
|
|
26
|
-
from fontTools.pens.boundsPen import BoundsPen
|
|
27
|
-
|
|
28
|
-
here = Path(__file__).resolve().parent
|
|
29
|
-
root = here.parents[1]
|
|
30
|
-
default_font = root / "assets/fonts/inter/inter-variable-latin.woff2"
|
|
31
|
-
|
|
32
|
-
chars = sys.argv[1] if len(sys.argv) > 1 else "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
33
|
-
weight = float(sys.argv[2]) if len(sys.argv) > 2 else 800.0
|
|
34
|
-
font_path = Path(sys.argv[3]).expanduser() if len(sys.argv) > 3 else default_font
|
|
35
|
-
|
|
36
|
-
if len(sys.argv) > 4:
|
|
37
|
-
out_path = Path(sys.argv[4])
|
|
38
|
-
if not out_path.is_absolute():
|
|
39
|
-
out_path = here / out_path
|
|
40
|
-
elif len(sys.argv) > 3:
|
|
41
|
-
# Custom font, no explicit output: derive a name so Inter is never clobbered.
|
|
42
|
-
out_path = here / f"{font_path.stem}-glyphs.json"
|
|
43
|
-
else:
|
|
44
|
-
out_path = here / "brand-glyphs.json"
|
|
45
|
-
|
|
46
|
-
font = TTFont(str(font_path))
|
|
47
|
-
if "fvar" in font:
|
|
48
|
-
instantiateVariableFont(font, {"wght": weight}, inplace=True)
|
|
49
|
-
|
|
50
|
-
glyph_set = font.getGlyphSet()
|
|
51
|
-
cmap = font.getBestCmap()
|
|
52
|
-
upem = font["head"].unitsPerEm
|
|
53
|
-
|
|
54
|
-
glyphs = {}
|
|
55
|
-
for ch in dict.fromkeys(chars):
|
|
56
|
-
name = cmap[ord(ch)]
|
|
57
|
-
g = glyph_set[name]
|
|
58
|
-
pen = SVGPathPen(glyph_set)
|
|
59
|
-
g.draw(pen)
|
|
60
|
-
bp = BoundsPen(glyph_set)
|
|
61
|
-
g.draw(bp)
|
|
62
|
-
# Blank glyphs (e.g. the space) have no contours, so BoundsPen leaves
|
|
63
|
-
# bounds None; they still carry an advance width the layout needs.
|
|
64
|
-
xmin, ymin, xmax, ymax = bp.bounds or (0, 0, 0, 0)
|
|
65
|
-
glyphs[ch] = {
|
|
66
|
-
"path": pen.getCommands(),
|
|
67
|
-
"advance": g.width,
|
|
68
|
-
"bounds": {"xMin": xmin, "yMin": ymin, "xMax": xmax, "yMax": ymax},
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
out_path.write_text(json.dumps(
|
|
72
|
-
{"font": font_path.name, "unitsPerEm": upem, "weight": weight, "glyphs": glyphs},
|
|
73
|
-
indent=2,
|
|
74
|
-
))
|
|
75
|
-
print(f"wrote {out_path.name} from {font_path.name} — upem={upem} weight={int(weight)} chars={''.join(glyphs)}")
|