mirror of
https://github.com/multipleof4/stain.otf.git
synced 2026-01-13 16:17:55 +00:00
Feat: Generate basic AI-style font
This commit is contained in:
153
scripts/build-font.js
Normal file
153
scripts/build-font.js
Normal file
@@ -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();
|
||||||
Reference in New Issue
Block a user