diff --git a/scripts/build-font.js b/scripts/build-font.js new file mode 100644 index 0000000..b1b2028 --- /dev/null +++ b/scripts/build-font.js @@ -0,0 +1,153 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import opentype from "opentype.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; + +// AI-style stub: deterministic parametric glyph generator +// This is where future AI (you call me) can map characters -> vector instructions. +function genGlyphPathForChar(ch) { + 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); + 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.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; +} + +function createGlyph(ch, advanceWidth = defaultWidth) { + const unicode = ch.charCodeAt(0); + const pathObj = genGlyphPathForChar(ch); + return new opentype.Glyph({ + name: ch === " " ? "space" : `uni${unicode.toString(16).toUpperCase()}`, + unicode, + advanceWidth, + path: pathObj + }); +} + +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 font = new opentype.Font({ + familyName: fontName, + styleName: "Regular", + unitsPerEm: upem, + ascender: ascent, + descender: descent, + glyphs + }); + + const ttfBuffer = Buffer.from(font.toArrayBuffer()); + + const outTTF = path.join(distDir, "StainFont-Basic.ttf"); + fs.writeFileSync(outTTF, ttfBuffer); + + console.log(`Built: ${outTTF}`); +} + +buildFont();