branding-engine 0.1.0 → 0.2.1

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.
Files changed (46) hide show
  1. package/README.md +428 -73
  2. package/bin/cli.mjs +8 -5
  3. package/examples/severino-labs/README.md +95 -0
  4. package/examples/severino-labs/brand.json +30 -0
  5. package/examples/severino-labs/generated/cards/social-card.png +0 -0
  6. package/examples/severino-labs/generated/severino-labs/README.md +49 -0
  7. package/examples/severino-labs/generated/severino-labs/icons/apple-touch-icon.png +0 -0
  8. package/examples/severino-labs/generated/severino-labs/icons/favicon-192.png +0 -0
  9. package/examples/severino-labs/generated/severino-labs/icons/favicon-32.png +0 -0
  10. package/examples/severino-labs/generated/severino-labs/icons/favicon.ico +0 -0
  11. package/examples/severino-labs/generated/severino-labs/icons/favicon.svg +1 -0
  12. package/examples/severino-labs/generated/severino-labs/mark/mark-1024.png +0 -0
  13. package/examples/severino-labs/generated/severino-labs/mark/mark-512.png +0 -0
  14. package/examples/severino-labs/generated/severino-labs/mark/mark-transparent-dark.png +0 -0
  15. package/examples/severino-labs/generated/severino-labs/mark/mark-transparent-light.png +0 -0
  16. package/examples/severino-labs/generated/severino-labs/mark/mark.svg +1 -0
  17. package/examples/severino-labs/generated/severino-labs/sheet/overview.png +0 -0
  18. package/examples/severino-labs/generated/severino-labs/sheet/palette.png +0 -0
  19. package/examples/severino-labs/generated/severino-labs/sheet/sheet-mark.png +0 -0
  20. package/examples/severino-labs/generated/severino-labs/sheet/type-specimen.png +0 -0
  21. package/examples/severino-labs/generated/severino-labs/web/head.html +6 -0
  22. package/examples/severino-labs/generated/severino-labs/web/site.webmanifest +19 -0
  23. package/examples/severino-labs/generated/severino-labs/web/tokens.css +7 -0
  24. package/examples/severino-labs/generated/severino-labs/wordmark/wordmark-caps-dark.png +0 -0
  25. package/examples/severino-labs/generated/severino-labs/wordmark/wordmark-caps-light.png +0 -0
  26. package/examples/severino-labs/generated/severino-labs/wordmark/wordmark-caps.svg +1 -0
  27. package/examples/severino-labs/generated/severino-labs/wordmark/wordmark-dark.png +0 -0
  28. package/examples/severino-labs/generated/severino-labs/wordmark/wordmark-light.png +0 -0
  29. package/examples/severino-labs/generated/severino-labs/wordmark/wordmark.svg +1 -0
  30. package/examples/severino-labs/studio.jpg +0 -0
  31. package/index.mjs +1 -0
  32. package/package.json +13 -6
  33. package/src/build.mjs +8 -6
  34. package/src/lib/extract-glyphs.mjs +65 -0
  35. package/src/lib/glyphs.mjs +6 -19
  36. package/src/lib/identity.mjs +11 -0
  37. package/src/lib/mark.mjs +28 -8
  38. package/src/lib/render.mjs +11 -2
  39. package/src/lib/wordmark.mjs +6 -1
  40. package/src/make-mark.mjs +2 -0
  41. package/src/make-sheet.mjs +2 -0
  42. package/src/make-web.mjs +2 -0
  43. package/src/make-wordmark.mjs +3 -1
  44. package/src/site.mjs +3 -1
  45. package/requirements.txt +0 -3
  46. package/src/lib/extract-glyphs.py +0 -75
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 });
@@ -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;
@@ -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,3 +0,0 @@
1
- # For ./glyphs.sh (glyph extraction). Install with: pip install -r requirements.txt
2
- fonttools>=4.60
3
- brotli>=1.1
@@ -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)}")