Delete glyphs.js

This commit is contained in:
2025-11-09 07:57:04 -08:00
parent d8e7e0aa91
commit 1245118b30

View File

@@ -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);
})();