Feat: Build readable Stain Regular font

This commit is contained in:
2025-11-08 22:50:25 -08:00
parent e010a0832a
commit 90860335a2

View File

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