diff --git a/src/glyphs.js b/src/glyphs.js deleted file mode 100644 index 09c2843..0000000 --- a/src/glyphs.js +++ /dev/null @@ -1,955 +0,0 @@ -import opentype from "opentype.js"; - -const UPEM = 1000; -const ASC = 760; -const DSC = -240; -const CAP = 700; -const XH = 520; -const BASE = 0; - -const STROKE = 78; -const ROUND = 26; - -const WIDTH = { - default: 560, - narrow: 500, - wide: 620, - space: 250 -}; - -const CHARSET = { - upper: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", - lower: "abcdefghijklmnopqrstuvwxyz", - digits: "0123456789" -}; - -const COMMON_PUNCT = ".,:;!?-–—()'\"@#$%&*+/=_"; - -function r(v) { - return Math.round(v); -} - -function hBar(y, x1, x2, s = STROKE) { - const p = new opentype.Path(); - p.moveTo(r(x1), r(y)); - p.lineTo(r(x2), r(y)); - p.lineTo(r(x2), r(y - s)); - p.lineTo(r(x1), r(y - s)); - p.close(); - return p; -} - -function vBar(x, y1, y2, s = STROKE) { - const p = new opentype.Path(); - p.moveTo(r(x), r(y1)); - p.lineTo(r(x + s), r(y1)); - p.lineTo(r(x + s), r(y2)); - p.lineTo(r(x), r(y2)); - p.close(); - return p; -} - -function roundRect(x, y, w, h, rr = ROUND) { - const r0 = Math.max(0, Math.min(rr, w / 2, h / 2)); - const p = new opentype.Path(); - const x2 = x + w; - const y2 = y + h; - - p.moveTo(r(x + r0), r(y)); - p.lineTo(r(x2 - r0), r(y)); - p.quadraticCurveTo(r(x2), r(y), r(x2), r(y + r0)); - p.lineTo(r(x2), r(y2 - r0)); - p.quadraticCurveTo(r(x2), r(y2), r(x2 - r0), r(y2)); - p.lineTo(r(x + r0), r(y2)); - p.quadraticCurveTo(r(x), r(y2), r(x), r(y2 - r0)); - p.lineTo(r(x), r(y + r0)); - p.quadraticCurveTo(r(x), r(y), r(x + r0), r(y)); - p.close(); - - return p; -} - -function mergePaths(...paths) { - const p = new opentype.Path(); - for (const part of paths) { - if (!part) continue; - if (part.commands) { - p.commands = p.commands.concat(part.commands); - } - } - return p; -} - -/* Uppercase glyphs: geometric, slightly rounded, Candara-ish */ - -const upperBuilders = { - "A": () => { - const aw = WIDTH.wide; - const mid = aw / 2; - const s = STROKE * 0.9; - const p = new opentype.Path(); - p.moveTo(r(STROKE), BASE); - p.lineTo(r(STROKE + s), BASE); - p.lineTo(r(mid), CAP); - p.lineTo(r(mid - s), CAP); - p.close(); - p.moveTo(r(aw - STROKE), BASE); - p.lineTo(r(aw - STROKE - s), BASE); - p.lineTo(r(mid - s * 0.3), CAP); - p.lineTo(r(mid + s * 0.3), CAP); - p.close(); - p.commands = p.commands.concat( - hBar(XH + 20, STROKE * 1.4, aw - STROKE * 1.4, STROKE * 0.78).commands - ); - return { path: p, width: aw }; - }, - "B": () => { - const aw = WIDTH.wide; - const left = STROKE * 0.8; - const stem = vBar(left, BASE, CAP, STROKE * 0.9); - const top = roundRect(left, CAP - 310, aw - left * 1.8, 160, 46); - const bot = roundRect(left, BASE + 70, aw - left * 1.8, 160, 46); - return { path: mergePaths(stem, top, bot), width: aw }; - }, - "C": () => { - const aw = WIDTH.wide; - const outer = roundRect(STROKE, BASE + 40, aw - STROKE * 2, CAP - 80, 120); - const inner = roundRect(STROKE + 70, BASE + 90, aw - STROKE * 2 - 140, CAP - 180, 90); - const p = mergePaths(outer, inner); - // knock right side open by overlaying a vertical gap - return { path: p, width: aw }; - }, - "D": () => { - const aw = WIDTH.wide; - const left = STROKE * 0.85; - const stem = vBar(left, BASE, CAP, STROKE * 0.9); - const bowl = roundRect(left, BASE + 40, aw - left * 1.5, CAP - 80, 90); - return { path: mergePaths(stem, bowl), width: aw }; - }, - "E": () => { - const aw = WIDTH.default; - const x = STROKE * 0.85; - return { - path: mergePaths( - vBar(x, BASE, CAP, STROKE * 0.9), - hBar(CAP, x, aw - STROKE * 0.5), - hBar(XH + 10, x, aw - STROKE * 0.9, STROKE * 0.75), - hBar(BASE + 40, x, aw - STROKE * 0.5) - ), - width: aw - }; - }, - "F": () => { - const aw = WIDTH.default; - const x = STROKE * 0.85; - return { - path: mergePaths( - vBar(x, BASE, CAP, STROKE * 0.9), - hBar(CAP, x, aw - STROKE * 0.5), - hBar(XH + 10, x, aw - STROKE * 0.9, STROKE * 0.75) - ), - width: aw - }; - }, - "G": () => { - const aw = WIDTH.wide; - const outer = roundRect(STROKE, BASE + 40, aw - STROKE * 2, CAP - 80, 110); - const inner = roundRect(STROKE + 70, BASE + 90, aw - STROKE * 2 - 160, CAP - 180, 80); - const cut = hBar(XH + 20, aw * 0.35, aw - STROKE * 0.8, STROKE * 0.8); - const p = mergePaths(outer, inner, cut); - return { path: p, width: aw }; - }, - "H": () => { - const aw = WIDTH.wide; - const l = STROKE * 0.85; - const rgt = aw - STROKE * 1.5; - return { - path: mergePaths( - vBar(l, BASE, CAP), - vBar(rgt, BASE, CAP), - hBar(XH + 20, l, rgt + STROKE) - ), - width: aw - }; - }, - "I": () => { - const aw = WIDTH.narrow; - const c = aw / 2 - STROKE / 2; - return { - path: mergePaths( - hBar(CAP, STROKE * 0.4, aw - STROKE * 0.4, STROKE * 0.8), - hBar(BASE + 40, STROKE * 0.4, aw - STROKE * 0.4, STROKE * 0.8), - vBar(c, BASE + 40 + STROKE * 0.5, CAP - STROKE * 1.4, STROKE * 0.8) - ), - width: aw - }; - }, - "J": () => { - const aw = WIDTH.default; - const top = hBar(CAP, STROKE * 0.4, aw - STROKE * 0.4, STROKE * 0.8); - const stem = vBar(aw - STROKE * 1.6, BASE + 110, CAP - STROKE, STROKE * 0.8); - const bowl = roundRect(aw * 0.35, BASE, aw * 0.4, 120, 50); - return { path: mergePaths(top, stem, bowl), width: aw }; - }, - "K": () => { - const aw = WIDTH.wide; - const l = STROKE * 0.85; - const s = STROKE * 0.8; - const p1 = vBar(l, BASE, CAP); - const p2 = new opentype.Path(); - p2.moveTo(aw - STROKE, CAP); - p2.lineTo(aw - STROKE - s, CAP); - p2.lineTo(l + STROKE * 1.5, XH + 40); - p2.lineTo(l + STROKE * 2.2, XH + 10); - p2.close(); - const p3 = new opentype.Path(); - p3.moveTo(aw - STROKE, BASE); - p3.lineTo(aw - STROKE - s, BASE + STROKE); - p3.lineTo(l + STROKE * 1.5, XH - 40); - p3.lineTo(l + STROKE * 2.2, XH - 10); - p3.close(); - return { path: mergePaths(p1, p2, p3), width: aw }; - }, - "L": () => { - const aw = WIDTH.default; - const l = STROKE * 0.85; - return { - path: mergePaths( - vBar(l, BASE, CAP), - hBar(BASE + 40, l, aw - STROKE * 0.5) - ), - width: aw - }; - }, - "M": () => { - const aw = WIDTH.wide + 40; - const s = STROKE * 0.9; - const left = vBar(STROKE, BASE, CAP, s); - const right = vBar(aw - STROKE * 1.8, BASE, CAP, s); - const mid = new opentype.Path(); - mid.moveTo(STROKE + s, CAP); - mid.lineTo(aw / 2, CAP * 0.45); - mid.lineTo(aw - STROKE * 1.8 - s, CAP); - mid.lineTo(aw - STROKE * 1.8 - s * 1.4, CAP); - mid.lineTo(aw / 2, CAP * 0.56); - mid.lineTo(STROKE + s * 1.4, CAP); - mid.close(); - return { path: mergePaths(left, right, mid), width: aw }; - }, - "N": () => { - const aw = WIDTH.wide; - const s = STROKE * 0.9; - const left = vBar(STROKE, BASE, CAP, s); - const right = vBar(aw - STROKE * 1.6, BASE, CAP, s); - const diag = new opentype.Path(); - diag.moveTo(STROKE + s, CAP); - diag.lineTo(STROKE + s * 1.7, CAP); - diag.lineTo(aw - STROKE * 1.6 - s, BASE); - diag.lineTo(aw - STROKE * 1.6 - s * 1.7, BASE); - diag.close(); - return { path: mergePaths(left, right, diag), width: aw }; - }, - "O": () => { - const aw = WIDTH.wide; - const outer = roundRect(STROKE, BASE + 40, aw - STROKE * 2, CAP - 80, 110); - const inner = roundRect(STROKE + 70, BASE + 90, aw - STROKE * 2 - 140, CAP - 180, 80); - return { path: mergePaths(outer, inner), width: aw }; - }, - "P": () => { - const aw = WIDTH.default; - const l = STROKE * 0.85; - const stem = vBar(l, BASE, CAP, STROKE * 0.9); - const bowl = roundRect(l, CAP - 290, aw - l * 1.7, 210, 60); - return { path: mergePaths(stem, bowl), width: aw }; - }, - "Q": () => { - const base = upperBuilders["O"](); - const aw = base.width; - const tail = new opentype.Path(); - tail.moveTo(aw * 0.55, BASE + 140); - tail.lineTo(aw * 0.82, BASE - 10); - tail.lineTo(aw * 0.76, BASE + 70); - tail.lineTo(aw * 0.50, BASE + 170); - tail.close(); - return { path: mergePaths(base.path, tail), width: aw }; - }, - "R": () => { - const aw = WIDTH.wide; - const l = STROKE * 0.85; - const stem = vBar(l, BASE, CAP, STROKE * 0.9); - const bowl = roundRect(l, CAP - 290, aw - l * 1.9, 210, 50); - const leg = new opentype.Path(); - leg.moveTo(aw - STROKE, BASE); - leg.lineTo(aw - STROKE - STROKE * 0.9, BASE + STROKE); - leg.lineTo(l + STROKE * 1.8, XH - 40); - leg.lineTo(l + STROKE * 2.4, XH - 80); - leg.close(); - return { path: mergePaths(stem, bowl, leg), width: aw }; - }, - "S": () => { - const aw = WIDTH.default; - const p = new opentype.Path(); - const top = CAP - 20; - const mid = (CAP + BASE) / 2; - const bot = BASE + 40; - p.moveTo(aw - 40, top); - p.quadraticCurveTo(aw * 0.2, CAP + 10, 60, mid + 90); - p.quadraticCurveTo(20, mid + 10, aw * 0.3, mid - 40); - p.quadraticCurveTo(aw - 20, mid - 90, aw - 40, bot - 10); - p.quadraticCurveTo(aw * 0.5, bot - 70, 80, bot + 10); - p.quadraticCurveTo(aw * 0.5, bot + 40, aw - 40, top - 40); - p.close(); - return { path: p, width: aw }; - }, - "T": () => { - const aw = WIDTH.wide; - const c = aw / 2 - STROKE / 2; - return { - path: mergePaths( - hBar(CAP, STROKE * 0.4, aw - STROKE * 0.4), - vBar(c, BASE, CAP, STROKE * 0.9) - ), - width: aw - }; - }, - "U": () => { - const aw = WIDTH.wide; - const s = STROKE * 0.9; - const left = vBar(STROKE, BASE + 140, CAP, s); - const right = vBar(aw - STROKE * 2, BASE + 140, CAP, s); - const bowl = roundRect(STROKE, BASE + 40, aw - STROKE * 3, 140, 80); - return { path: mergePaths(left, right, bowl), width: aw }; - }, - "V": () => { - const aw = WIDTH.wide; - const s = STROKE * 0.9; - const mid = aw / 2; - const p1 = new opentype.Path(); - p1.moveTo(STROKE, CAP); - p1.lineTo(STROKE + s, CAP); - p1.lineTo(mid, BASE); - p1.lineTo(mid - s, BASE); - p1.close(); - const p2 = new opentype.Path(); - p2.moveTo(aw - STROKE, CAP); - p2.lineTo(aw - STROKE - s, CAP); - p2.lineTo(mid, BASE); - p2.lineTo(mid + s, BASE); - p2.close(); - return { path: mergePaths(p1, p2), width: aw }; - }, - "W": () => { - const aw = WIDTH.wide + 80; - const s = STROKE * 0.8; - const x1 = STROKE; - const x2 = aw * 0.34; - const x3 = aw * 0.66; - const x4 = aw - STROKE; - const yTop = CAP; - const yBot = BASE; - const p = new opentype.Path(); - p.moveTo(x1, yTop); - p.lineTo(x1 + s, yTop); - p.lineTo(x2, yBot); - p.lineTo(x2 - s, yBot); - p.close(); - p.moveTo(x2, yBot); - p.lineTo(x2 + s, yBot); - p.lineTo(x3, yTop * 0.52); - p.lineTo(x3 - s, yTop * 0.52); - p.close(); - p.moveTo(x3, yTop * 0.52); - p.lineTo(x3 + s, yTop * 0.52); - p.lineTo(x4, yTop); - p.lineTo(x4 - s, yTop); - p.close(); - return { path: p, width: aw }; - }, - "X": () => { - const aw = WIDTH.wide; - const s = STROKE * 0.8; - const p1 = new opentype.Path(); - p1.moveTo(STROKE, CAP); - p1.lineTo(STROKE + s, CAP); - p1.lineTo(aw - STROKE, BASE); - p1.lineTo(aw - STROKE - s, BASE); - p1.close(); - const p2 = new opentype.Path(); - p2.moveTo(aw - STROKE, CAP); - p2.lineTo(aw - STROKE - s, CAP); - p2.lineTo(STROKE, BASE); - p2.lineTo(STROKE + s, BASE); - p2.close(); - return { path: mergePaths(p1, p2), width: aw }; - }, - "Y": () => { - const aw = WIDTH.wide; - const s = STROKE * 0.85; - const mid = aw / 2; - const arms1 = new opentype.Path(); - arms1.moveTo(STROKE, CAP); - arms1.lineTo(STROKE + s, CAP); - arms1.lineTo(mid, XH); - arms1.lineTo(mid - s, XH); - arms1.close(); - const arms2 = new opentype.Path(); - arms2.moveTo(aw - STROKE, CAP); - arms2.lineTo(aw - STROKE - s, CAP); - arms2.lineTo(mid, XH); - arms2.lineTo(mid + s, XH); - arms2.close(); - const stem = vBar(mid - s / 2, BASE, XH, s * 0.9); - return { path: mergePaths(arms1, arms2, stem), width: aw }; - }, - "Z": () => { - const aw = WIDTH.wide; - const top = hBar(CAP, STROKE * 0.4, aw - STROKE * 0.4); - const bot = hBar(BASE + 40, STROKE * 0.6, aw - STROKE * 0.4); - const diag = new opentype.Path(); - diag.moveTo(aw - STROKE * 0.9, CAP - STROKE); - diag.lineTo(aw - STROKE * 1.5, CAP - STROKE); - diag.lineTo(STROKE * 0.7, BASE + 40 + STROKE); - diag.lineTo(STROKE * 1.3, BASE + 40 + STROKE); - diag.close(); - return { path: mergePaths(top, bot, diag), width: aw }; - } -}; - -/* Lowercase: softer, shorter, consistent with uppercase rhythm */ - -function lcStemLeft() { - return STROKE * 0.9; -} -function lcXWidth() { - return WIDTH.default; -} - -const lowerBuilders = { - "a": () => { - const aw = lcXWidth(); - const bowl = roundRect(60, BASE + 80, aw - 120, XH - 100, 70); - const inner = roundRect(90, BASE + 110, aw - 180, XH - 160, 50); - const stem = vBar(aw - 120, XH - 40, XH + 40, STROKE * 0.7); - return { path: mergePaths(bowl, inner, stem), width: aw }; - }, - "b": () => { - const aw = lcXWidth(); - const x = lcStemLeft(); - const stem = vBar(x, BASE, CAP, STROKE * 0.8); - const bowl = roundRect(x, BASE + 80, aw - x - 40, XH - 80, 70); - return { path: mergePaths(stem, bowl), width: aw }; - }, - "c": () => { - const aw = lcXWidth(); - const outer = roundRect(60, BASE + 80, aw - 120, XH - 80, 80); - const inner = roundRect(100, BASE + 110, aw - 200, XH - 140, 60); - return { path: mergePaths(outer, inner), width: aw }; - }, - "d": () => { - const aw = lcXWidth(); - const stemX = aw - lcStemLeft() - STROKE * 0.2; - const stem = vBar(stemX, BASE, CAP, STROKE * 0.8); - const bowl = roundRect(60, BASE + 80, aw - 120, XH - 80, 70); - return { path: mergePaths(bowl, stem), width: aw }; - }, - "e": () => { - const aw = lcXWidth(); - const outer = roundRect(60, BASE + 80, aw - 120, XH - 80, 80); - const bar = hBar(XH - 20, 80, aw - 80, STROKE * 0.6); - return { path: mergePaths(outer, bar), width: aw }; - }, - "f": () => { - const aw = WIDTH.narrow; - const x = lcStemLeft(); - const stem = vBar(x, BASE - 40, CAP, STROKE * 0.7); - const bar = hBar(XH, x - 10, aw - 40, STROKE * 0.55); - return { path: mergePaths(stem, bar), width: aw }; - }, - "g": () => { - const aw = lcXWidth(); - const bowl = roundRect(60, BASE + 80, aw - 120, XH - 80, 70); - const tail = roundRect(aw / 2 - 40, DSC, 80, BASE + 80 - DSC, 40); - return { path: mergePaths(bowl, tail), width: aw }; - }, - "h": () => { - const aw = lcXWidth(); - const x = lcStemLeft(); - const stem = vBar(x, BASE, CAP, STROKE * 0.8); - const arch = roundRect(x, XH - 80, aw - x - 40, 80, 50); - return { path: mergePaths(stem, arch), width: aw }; - }, - "i": () => { - const aw = WIDTH.narrow; - const stem = vBar(aw / 2 - STROKE * 0.35, BASE, XH, STROKE * 0.7); - const dot = roundRect(aw / 2 - 18, XH + 60, 36, 36, 18); - return { path: mergePaths(stem, dot), width: aw }; - }, - "j": () => { - const aw = WIDTH.narrow; - const stem = vBar(aw / 2 - STROKE * 0.35, DSC, XH, STROKE * 0.7); - const dot = roundRect(aw / 2 - 18, XH + 60, 36, 36, 18); - return { path: mergePaths(stem, dot), width: aw }; - }, - "k": () => { - const aw = lcXWidth(); - const x = lcStemLeft(); - const stem = vBar(x, BASE, CAP, STROKE * 0.8); - const arm = new opentype.Path(); - arm.moveTo(x + STROKE * 0.8, XH - 10); - arm.lineTo(aw - 40, BASE); - arm.lineTo(aw - 70, BASE); - arm.lineTo(x + STROKE * 0.6, XH - 40); - arm.close(); - const arm2 = new opentype.Path(); - arm2.moveTo(x + STROKE * 0.8, XH + 10); - arm2.lineTo(aw - 60, CAP * 0.55); - arm2.lineTo(aw - 90, CAP * 0.55); - arm2.lineTo(x + STROKE * 0.6, XH + 40); - arm2.close(); - return { path: mergePaths(stem, arm, arm2), width: aw }; - }, - "l": () => { - const aw = WIDTH.narrow; - const stem = vBar(aw / 2 - STROKE * 0.35, BASE, CAP, STROKE * 0.7); - return { path: stem, width: aw }; - }, - "m": () => { - const aw = WIDTH.wide + 40; - const x = lcStemLeft(); - const s1 = vBar(x, BASE, XH, STROKE * 0.7); - const arch1 = roundRect(x, XH - 70, (aw - x) / 2 - 40, 70, 40); - const s2x = x + (aw - x) / 2 - 20; - const s2 = vBar(s2x, BASE, XH, STROKE * 0.7); - const arch2 = roundRect(s2x, XH - 70, aw - s2x - 40, 70, 40); - return { path: mergePaths(s1, arch1, s2, arch2), width: aw }; - }, - "n": () => { - const aw = lcXWidth(); - const x = lcStemLeft(); - const stem = vBar(x, BASE, XH, STROKE * 0.7); - const arch = roundRect(x, XH - 70, aw - x - 40, 70, 40); - return { path: mergePaths(stem, arch), width: aw }; - }, - "o": () => { - const aw = lcXWidth(); - const outer = roundRect(60, BASE + 80, aw - 120, XH - 80, 80); - const inner = roundRect(95, BASE + 110, aw - 190, XH - 140, 60); - return { path: mergePaths(outer, inner), width: aw }; - }, - "p": () => { - const aw = lcXWidth(); - const x = lcStemLeft(); - const stem = vBar(x, DSC, XH, STROKE * 0.8); - const bowl = roundRect(x, BASE + 80, aw - x - 40, XH - 80, 70); - return { path: mergePaths(stem, bowl), width: aw }; - }, - "q": () => { - const aw = lcXWidth(); - const stemX = aw - lcStemLeft() - STROKE * 0.2; - const stem = vBar(stemX, DSC, XH, STROKE * 0.8); - const bowl = roundRect(60, BASE + 80, aw - 120, XH - 80, 70); - return { path: mergePaths(bowl, stem), width: aw }; - }, - "r": () => { - const aw = WIDTH.default; - const x = lcStemLeft(); - const stem = vBar(x, BASE, XH, STROKE * 0.7); - const shoulder = hBar(XH - 10, x + STROKE * 0.6, aw - 80, STROKE * 0.55); - return { path: mergePaths(stem, shoulder), width: aw }; - }, - "s": () => { - const aw = lcXWidth(); - const p = new opentype.Path(); - const top = XH - 10; - const mid = (XH + BASE + 80) / 2; - const bot = BASE + 80; - p.moveTo(aw - 40, top); - p.quadraticCurveTo(aw * 0.2, XH + 20, 60, mid + 30); - p.quadraticCurveTo(20, mid - 10, aw * 0.3, mid - 40); - p.quadraticCurveTo(aw - 20, mid - 70, aw - 40, bot - 10); - p.quadraticCurveTo(aw * 0.5, bot - 40, 60, bot + 10); - p.quadraticCurveTo(aw * 0.6, bot + 20, aw - 40, top - 40); - p.close(); - return { path: p, width: aw }; - }, - "t": () => { - const aw = WIDTH.narrow; - const stem = vBar(aw / 2 - STROKE * 0.35, BASE, CAP - 40, STROKE * 0.7); - const bar = hBar(XH, 40, aw - 40, STROKE * 0.55); - return { path: mergePaths(stem, bar), width: aw }; - }, - "u": () => { - const aw = lcXWidth(); - const bowl = roundRect(60, BASE + 40, aw - 120, XH - 40, 70); - const rightStem = vBar(aw - 100, BASE + 40, XH, STROKE * 0.7); - return { path: mergePaths(bowl, rightStem), width: aw }; - }, - "v": () => { - const aw = lcXWidth(); - const s = STROKE * 0.7; - const mid = aw / 2; - const p1 = new opentype.Path(); - p1.moveTo(60, XH); - p1.lineTo(60 + s, XH); - p1.lineTo(mid, BASE); - p1.lineTo(mid - s, BASE); - p1.close(); - const p2 = new opentype.Path(); - p2.moveTo(aw - 60, XH); - p2.lineTo(aw - 60 - s, XH); - p2.lineTo(mid, BASE); - p2.lineTo(mid + s, BASE); - p2.close(); - return { path: mergePaths(p1, p2), width: aw }; - }, - "w": () => { - const aw = WIDTH.wide + 40; - const s = STROKE * 0.65; - const x1 = 50; - const x2 = aw * 0.36; - const x3 = aw * 0.64; - const x4 = aw - 50; - const yTop = XH; - const yBot = BASE; - const p = new opentype.Path(); - p.moveTo(x1, yTop); - p.lineTo(x1 + s, yTop); - p.lineTo(x2, yBot); - p.lineTo(x2 - s, yBot); - p.close(); - p.moveTo(x2, yBot); - p.lineTo(x2 + s, yBot); - p.lineTo(x3, yTop * 0.55); - p.lineTo(x3 - s, yTop * 0.55); - p.close(); - p.moveTo(x3, yTop * 0.55); - p.lineTo(x3 + s, yTop * 0.55); - p.lineTo(x4, yTop); - p.lineTo(x4 - s, yTop); - p.close(); - return { path: p, width: aw }; - }, - "x": () => { - const aw = lcXWidth(); - const s = STROKE * 0.65; - const p1 = new opentype.Path(); - p1.moveTo(60, XH); - p1.lineTo(60 + s, XH); - p1.lineTo(aw - 60, BASE); - p1.lineTo(aw - 60 - s, BASE); - p1.close(); - const p2 = new opentype.Path(); - p2.moveTo(aw - 60, XH); - p2.lineTo(aw - 60 - s, XH); - p2.lineTo(60, BASE); - p2.lineTo(60 + s, BASE); - p2.close(); - return { path: mergePaths(p1, p2), width: aw }; - }, - "y": () => { - const aw = lcXWidth(); - const s = STROKE * 0.7; - const mid = aw / 2; - const p1 = new opentype.Path(); - p1.moveTo(60, XH); - p1.lineTo(60 + s, XH); - p1.lineTo(mid, BASE); - p1.lineTo(mid - s, BASE); - p1.close(); - const p2 = new opentype.Path(); - p2.moveTo(aw - 60, XH); - p2.lineTo(aw - 60 - s, XH); - p2.lineTo(mid, DSC); - p2.lineTo(mid + s, DSC); - p2.close(); - return { path: mergePaths(p1, p2), width: aw }; - }, - "z": () => { - const aw = lcXWidth(); - const top = hBar(XH, 60, aw - 60, STROKE * 0.55); - const bot = hBar(BASE + 40, 60, aw - 60, STROKE * 0.55); - const diag = new opentype.Path(); - diag.moveTo(aw - 60, XH - STROKE * 0.5); - diag.lineTo(aw - 80, XH - STROKE * 0.5); - diag.lineTo(60, BASE + 40 + STROKE * 0.5); - diag.lineTo(80, BASE + 40 + STROKE * 0.5); - diag.close(); - return { path: mergePaths(top, bot, diag), width: aw }; - } -}; - -/* Digits: rounded rectangular, consistent weight */ - -function digitBuilder(d) { - const aw = WIDTH.default; - switch (d) { - case "0": { - const outer = roundRect(80, BASE + 40, aw - 160, CAP - 160, 90); - const inner = roundRect(120, BASE + 80, aw - 240, CAP - 240, 70); - return { path: mergePaths(outer, inner), width: aw }; - } - case "1": { - const stem = vBar(aw / 2 - STROKE * 0.4, BASE + 40, CAP - 40, STROKE * 0.8); - const base = hBar(BASE + 40, aw / 2 - 80, aw / 2 + 80, STROKE * 0.7); - return { path: mergePaths(stem, base), width: aw }; - } - case "2": { - const top = hBar(CAP - 40, 80, aw - 80, STROKE * 0.7); - const curve = new opentype.Path(); - curve.moveTo(aw - 80, CAP - 40); - curve.quadraticCurveTo(aw - 10, XH + 40, aw * 0.4, XH); - curve.quadraticCurveTo(80, XH - 40, 80, BASE + 40); - const base = hBar(BASE + 40, 80, aw - 80, STROKE * 0.7); - return { path: mergePaths(top, curve, base), width: aw }; - } - case "3": { - const top = roundRect(80, CAP - 260, aw - 160, 90, 40); - const bot = roundRect(80, BASE + 80, aw - 160, 90, 40); - const mid = new opentype.Path(); - mid.moveTo(aw - 80, CAP - 170); - mid.quadraticCurveTo(aw, XH, aw - 80, BASE + 170); - mid.quadraticCurveTo(aw - 40, BASE + 210, aw - 40, BASE + 230); - return { path: mergePaths(top, bot, mid), width: aw }; - } - case "4": { - const stem = vBar(aw - 120, BASE + 40, CAP, STROKE * 0.8); - const diag = new opentype.Path(); - diag.moveTo(80, CAP - 180); - diag.lineTo(80 + STROKE * 0.7, CAP - 180); - diag.lineTo(aw - 120, BASE + 40); - diag.lineTo(aw - 140, BASE + 40); - diag.close(); - const bar = hBar(CAP - 200, 80, aw - 120, STROKE * 0.7); - return { path: mergePaths(stem, diag, bar), width: aw }; - } - case "5": { - const top = hBar(CAP - 40, 80, aw - 80, STROKE * 0.7); - const left = vBar(80, CAP - 260, CAP - 40, STROKE * 0.7); - const mid = hBar(CAP - 260, 80, aw - 80, STROKE * 0.7); - const bowl = roundRect(80, BASE + 80, aw - 160, 120, 60); - return { path: mergePaths(top, left, mid, bowl), width: aw }; - } - case "6": { - const loop = roundRect(80, BASE + 40, aw - 160, CAP - 220, 80); - const inner = roundRect(120, BASE + 80, aw - 240, CAP - 260, 60); - const hook = new opentype.Path(); - hook.moveTo(aw - 80, CAP - 220); - hook.quadraticCurveTo(aw - 10, CAP - 260, aw - 60, CAP - 320); - return { path: mergePaths(loop, inner, hook), width: aw }; - } - case "7": { - const top = hBar(CAP - 40, 80, aw - 80, STROKE * 0.7); - const diag = new opentype.Path(); - diag.moveTo(aw - 80, CAP - 40); - diag.lineTo(aw - 80 - STROKE * 0.8, CAP - 40); - diag.lineTo(80, BASE + 40); - diag.lineTo(80 + STROKE * 0.8, BASE + 40); - diag.close(); - return { path: mergePaths(top, diag), width: aw }; - } - case "8": { - const top = roundRect(90, CAP - 260, aw - 180, 110, 50); - const bot = roundRect(90, BASE + 60, aw - 180, 140, 60); - const midHole = roundRect(120, BASE + 110, aw - 240, 60, 30); - return { path: mergePaths(top, bot, midHole), width: aw }; - } - case "9": { - const loop = roundRect(80, BASE + 160, aw - 160, CAP - 220, 80); - const inner = roundRect(120, BASE + 180, aw - 240, CAP - 260, 60); - const hook = new opentype.Path(); - hook.moveTo(80, BASE + 160); - hook.quadraticCurveTo(40, BASE + 120, 80, BASE + 80); - return { path: mergePaths(loop, inner, hook), width: aw }; - } - default: - return { path: new opentype.Path(), width: aw }; - } -} - -/* Punctuation & symbols */ - -function punctBuilder(ch) { - const aw = WIDTH.default; - if (ch === ".") { - return { - path: roundRect(aw / 2 - 16, BASE + 40, 32, 40, 16), - width: WIDTH.space - }; - } - if (ch === ",") { - const p = roundRect(aw / 2 - 16, BASE + 40, 32, 40, 16); - const tail = new opentype.Path(); - tail.moveTo(aw / 2 + 4, BASE + 40); - tail.lineTo(aw / 2 + 20, BASE - 10); - tail.lineTo(aw / 2 + 4, BASE); - tail.close(); - return { path: mergePaths(p, tail), width: WIDTH.space }; - } - if (ch === ":" || ch === ";") { - const top = roundRect(aw / 2 - 16, XH - 40, 32, 40, 16); - const bot = roundRect(aw / 2 - 16, BASE + 40, 32, 40, 16); - let p = mergePaths(top, bot); - if (ch === ";") { - const tail = new opentype.Path(); - tail.moveTo(aw / 2 + 4, BASE + 40); - tail.lineTo(aw / 2 + 20, BASE - 10); - tail.lineTo(aw / 2 + 4, BASE); - tail.close(); - p = mergePaths(p, tail); - } - return { path: p, width: WIDTH.space }; - } - if (ch === "!") { - const stem = vBar(aw / 2 - STROKE * 0.3, BASE + 120, CAP, STROKE * 0.6); - const dot = roundRect(aw / 2 - 16, BASE + 40, 32, 40, 16); - return { path: mergePaths(stem, dot), width: WIDTH.narrow }; - } - if (ch === "?") { - const hook = new opentype.Path(); - hook.moveTo(aw / 2 - 40, CAP - 180); - hook.quadraticCurveTo(aw - 40, CAP, aw - 40, XH); - hook.quadraticCurveTo(aw - 40, XH - 40, aw / 2, XH - 60); - hook.lineTo(aw / 2, BASE + 120); - const dot = roundRect(aw / 2 - 16, BASE + 40, 32, 40, 16); - return { path: mergePaths(hook, dot), width: aw }; - } - if (ch === "-" || ch === "–" || ch === "—") { - const len = ch === "-" ? aw * 0.4 : ch === "–" ? aw * 0.6 : aw * 0.9; - const bar = hBar(BASE + 260, (aw - len) / 2, (aw + len) / 2, STROKE * 0.4); - return { path: bar, width: len + 80 }; - } - if (ch === "(" || ch === ")") { - const p = new opentype.Path(); - const left = ch === "("; - const x = left ? aw / 2 - 40 : aw / 2 + 40; - const dir = left ? -1 : 1; - p.moveTo(x + dir * 20, CAP - 40); - p.quadraticCurveTo(x + dir * 80, (CAP + BASE) / 2, x + dir * 20, BASE + 40); - p.quadraticCurveTo(x + dir * 40, BASE + 80, x + dir * 40, BASE + 120); - return { path: p, width: WIDTH.space + 40 }; - } - if (ch === "'") { - const p = new opentype.Path(); - p.moveTo(aw / 2 - 10, CAP); - p.lineTo(aw / 2 + 10, CAP); - p.lineTo(aw / 2, CAP - 60); - p.close(); - return { path: p, width: WIDTH.space }; - } - if (ch === "\"") { - const left = punctBuilder("'").path; - const right = new opentype.Path(); - right.commands = left.commands.map(cmd => { - const c = { ...cmd }; - if (c.x !== undefined) c.x += 24; - if (c.x1 !== undefined) c.x1 += 24; - if (c.x2 !== undefined) c.x2 += 24; - return c; - }); - return { path: mergePaths(left, right), width: WIDTH.space + 16 }; - } - if (ch === "@") { - const outer = roundRect(40, BASE + 40, aw + 120, CAP - 160, 90); - const inner = roundRect(120, BASE + 80, aw, CAP - 220, 70); - return { path: mergePaths(outer, inner), width: aw + 160 }; - } - if (ch === "#") { - const bar1 = hBar(XH + 40, 80, aw - 80, STROKE * 0.35); - const bar2 = hBar(BASE + 200, 80, aw - 80, STROKE * 0.35); - const v1 = vBar(aw / 2 - 70, BASE + 40, CAP - 40, STROKE * 0.35); - const v2 = vBar(aw / 2 + 20, BASE + 40, CAP - 40, STROKE * 0.35); - return { path: mergePaths(bar1, bar2, v1, v2), width: aw }; - } - if (ch === "$") { - const s = digitBuilder("5").path; - return { path: s, width: aw }; - } - if (ch === "%") { - const slash = new opentype.Path(); - slash.moveTo(80, BASE + 40); - slash.lineTo(120, BASE + 40); - slash.lineTo(aw - 80, CAP - 40); - slash.lineTo(aw - 120, CAP - 40); - slash.close(); - const o1 = roundRect(80, CAP - 220, 80, 80, 30); - const o2 = roundRect(aw - 160, BASE + 40, 80, 80, 30); - return { path: mergePaths(slash, o1, o2), width: aw }; - } - if (ch === "&") { - const p = new opentype.Path(); - p.moveTo(aw - 80, CAP - 220); - p.quadraticCurveTo(aw - 40, CAP - 320, aw / 2, CAP - 320); - p.quadraticCurveTo(40, CAP - 320, 40, CAP - 200); - p.quadraticCurveTo(40, CAP - 80, aw / 2, CAP - 80); - p.quadraticCurveTo(aw, CAP - 60, aw / 2, BASE + 40); - p.quadraticCurveTo(40, BASE, aw / 2, BASE); - return { path: p, width: aw }; - } - if (ch === "*") { - const p = new opentype.Path(); - const cx = aw / 2; - const cy = (BASE + CAP) / 2; - const r0 = 40; - for (let i = 0; i < 6; i++) { - const a1 = (Math.PI * 2 * i) / 6; - const a2 = (Math.PI * 2 * (i + 1)) / 6; - p.moveTo(cx, cy); - p.lineTo(cx + r0 * Math.cos(a1), cy + r0 * Math.sin(a1)); - p.lineTo(cx + r0 * Math.cos(a2), cy + r0 * Math.sin(a2)); - p.close(); - } - return { path: p, width: aw }; - } - if (ch === "+") { - const h = hBar((BASE + CAP) / 2 + STROKE * 0.2, 80, aw - 80, STROKE * 0.5); - const v = vBar(aw / 2 - STROKE * 0.25, BASE + 80, CAP - 80, STROKE * 0.5); - return { path: mergePaths(h, v), width: aw }; - } - if (ch === "/") { - const p = new opentype.Path(); - p.moveTo(aw - 80, CAP); - p.lineTo(aw - 120, CAP); - p.lineTo(80, BASE); - p.lineTo(120, BASE); - p.close(); - return { path: p, width: aw }; - } - if (ch === "=") { - const h1 = hBar((BASE + CAP) / 2 + 40, 80, aw - 80, STROKE * 0.4); - const h2 = hBar((BASE + CAP) / 2 - 40, 80, aw - 80, STROKE * 0.4); - return { path: mergePaths(h1, h2), width: aw }; - } - if (ch === "_") { - const bar = hBar(BASE + 10, 40, aw - 40, STROKE * 0.25); - return { path: bar, width: aw }; - } - return { path: new opentype.Path(), width: aw }; -} - -/* Public builder */ - -export function buildGlyph(ch) { - if (ch === " ") { - return { path: new opentype.Path(), width: WIDTH.space }; - } - if (upperBuilders[ch]) return upperBuilders[ch](); - if (lowerBuilders[ch]) return lowerBuilders[ch](); - if (CHARSET.digits.includes(ch)) return digitBuilder(ch); - if (COMMON_PUNCT.includes(ch)) return punctBuilder(ch); - // Fallback: empty but with width, to avoid system fallback. - return { path: new opentype.Path(), width: WIDTH.default }; -} - -export const METRICS = { - unitsPerEm: UPEM, - ascender: ASC, - descender: DSC -}; - -export const SUPPORTED_CHARS = (() => { - const set = new Set(); - for (const s of Object.values(CHARSET)) for (const ch of s) set.add(ch); - for (const ch of COMMON_PUNCT) set.add(ch); - set.add(" "); - return Array.from(set); -})();