From 90860335a2aa45db89c8e1a4f76b94b88b86fccc Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Sat, 8 Nov 2025 22:50:25 -0800 Subject: [PATCH] Feat: Build readable Stain Regular font --- scripts/build-font.js | 166 +++++++++++------------------------------- 1 file changed, 44 insertions(+), 122 deletions(-) diff --git a/scripts/build-font.js b/scripts/build-font.js index b1b2028..32172e6 100644 --- a/scripts/build-font.js +++ b/scripts/build-font.js @@ -2,152 +2,74 @@ import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import opentype from "opentype.js"; +import { buildGlyph, METRICS } from "../src/glyphs.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); - const distDir = path.join(__dirname, "..", "dist"); if (!fs.existsSync(distDir)) fs.mkdirSync(distDir, { recursive: true }); -const upem = 1000; -const ascent = 800; -const descent = -200; -const defaultWidth = 600; +const FAMILY = "Stain"; +const STYLE = "Regular"; -// AI-style stub: deterministic parametric glyph generator -// This is where future AI (you call me) can map characters -> vector instructions. -function genGlyphPathForChar(ch) { +const charset = [ + " ", + "A","B","C","D","E","F","G","H","I","J","K","L","M", + "N","O","P","Q","R","S","T","U","V","W","X","Y","Z", + "0","1","2","3","4","5","6","7","8","9", + ".",",",":",";","!","?","-","–","—","@" +]; + +function buildNotdef() { + const w = 600; const p = new opentype.Path(); - const code = ch.charCodeAt(0); - - const seed = (code * 73) % 997; - const thickness = 40 + (seed % 60); - const inset = 80 + (seed % 120); - const cx = defaultWidth / 2; - - // Vertical stem - p.moveTo(cx - thickness / 2, ascent); - p.lineTo(cx + thickness / 2, ascent); - p.lineTo(cx + thickness / 2, descent); - p.lineTo(cx - thickness / 2, descent); + const m = 60; + p.moveTo(m, METRICS.descender); + p.lineTo(w - m, METRICS.descender); + p.lineTo(w - m, METRICS.ascender); + p.lineTo(m, METRICS.ascender); p.close(); - - // Top stain-like slab - const slabW = 260 + (seed % 140); - const slabH = 80 + (seed % 50); - p.moveTo(cx - slabW / 2, ascent); - p.lineTo(cx + slabW / 2, ascent); - p.lineTo(cx + slabW / 2, ascent - slabH); - p.quadraticCurveTo( - cx, - ascent - slabH - (seed % 40), - cx - slabW / 2, - ascent - slabH - ); + p.moveTo(m + 60, METRICS.descender + 60); + p.lineTo(w - m - 60, METRICS.descender + 60); + p.lineTo(w - m - 60, METRICS.ascender - 60); + p.lineTo(m + 60, METRICS.ascender - 60); p.close(); - - // Bottom irregular notch - const notchW = 120 + (seed % 80); - const notchY = descent + 40 + (seed % 60); - p.moveTo(cx - notchW / 2, notchY); - p.lineTo(cx + notchW / 2, notchY); - p.lineTo(cx + notchW / 2, descent); - p.quadraticCurveTo( - cx, - descent - (seed % 50), - cx - notchW / 2, - descent - ); - p.close(); - - // Inner void - const inner = inset; - if (inner + thickness * 1.5 < defaultWidth / 2) { - const iw = defaultWidth - inner * 2; - const ih = ascent - inner - 200; - if (iw > 80 && ih > 80) { - p.moveTo(inner, ih); - p.lineTo(inner + iw, ih); - p.lineTo(inner + iw, inner + 80); - p.lineTo(inner, inner + 80); - p.close(); - } - } - - return p; + return new opentype.Glyph({ + name: ".notdef", + unicode: 0, + advanceWidth: w, + path: p + }); } -function createGlyph(ch, advanceWidth = defaultWidth) { - const unicode = ch.charCodeAt(0); - const pathObj = genGlyphPathForChar(ch); +function createGlyph(ch) { + const unicode = ch.codePointAt(0); + const { path, width } = buildGlyph(ch); return new opentype.Glyph({ name: ch === " " ? "space" : `uni${unicode.toString(16).toUpperCase()}`, unicode, - advanceWidth, - path: pathObj + advanceWidth: width, + path }); } function buildFont() { - const fontName = "StainFont Basic"; - const chars = [ - " ", - "A","B","C","D","E","F","G","H","I","J","K","L","M", - "N","O","P","Q","R","S","T","U","V","W","X","Y","Z", - "0","1","2","3","4","5","6","7","8","9", - ".","!", "?", "@", "#", "-", "_", ":", ";", "," - ]; - - const glyphs = []; - - glyphs.push(new opentype.Glyph({ - name: ".notdef", - unicode: 0, - advanceWidth: defaultWidth, - path: (() => { - const p = new opentype.Path(); - p.moveTo(80, descent); - p.lineTo(defaultWidth - 80, descent); - p.lineTo(defaultWidth - 80, ascent); - p.lineTo(80, ascent); - p.close(); - p.moveTo(140, descent + 60); - p.lineTo(defaultWidth - 140, descent + 60); - p.lineTo(defaultWidth - 140, ascent - 60); - p.lineTo(140, ascent - 60); - p.close(); - return p; - })() - })); - - chars.forEach(ch => { - if (ch === " ") { - glyphs.push(new opentype.Glyph({ - name: "space", - unicode: 32, - advanceWidth: defaultWidth * 0.5, - path: new opentype.Path() - })); - } else { - glyphs.push(createGlyph(ch)); - } - }); + const glyphs = [buildNotdef()]; + charset.forEach(ch => glyphs.push(createGlyph(ch))); const font = new opentype.Font({ - familyName: fontName, - styleName: "Regular", - unitsPerEm: upem, - ascender: ascent, - descender: descent, + familyName: FAMILY, + styleName: STYLE, + unitsPerEm: METRICS.unitsPerEm, + ascender: METRICS.ascender, + descender: METRICS.descender, glyphs }); - const ttfBuffer = Buffer.from(font.toArrayBuffer()); - - const outTTF = path.join(distDir, "StainFont-Basic.ttf"); - fs.writeFileSync(outTTF, ttfBuffer); - - console.log(`Built: ${outTTF}`); + const ttf = Buffer.from(font.toArrayBuffer()); + const out = path.join(distDir, "Stain-Regular.ttf"); + fs.writeFileSync(out, ttf); + console.log(`Built: ${out}`); } buildFont();