mirror of
https://github.com/multipleof4/stain.otf.git
synced 2026-01-14 00:27:55 +00:00
Feat: Build readable Stain Regular font
This commit is contained in:
@@ -2,152 +2,74 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import opentype from "opentype.js";
|
import opentype from "opentype.js";
|
||||||
|
import { buildGlyph, METRICS } from "../src/glyphs.js";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const distDir = path.join(__dirname, "..", "dist");
|
const distDir = path.join(__dirname, "..", "dist");
|
||||||
if (!fs.existsSync(distDir)) fs.mkdirSync(distDir, { recursive: true });
|
if (!fs.existsSync(distDir)) fs.mkdirSync(distDir, { recursive: true });
|
||||||
|
|
||||||
const upem = 1000;
|
const FAMILY = "Stain";
|
||||||
const ascent = 800;
|
const STYLE = "Regular";
|
||||||
const descent = -200;
|
|
||||||
const defaultWidth = 600;
|
|
||||||
|
|
||||||
// AI-style stub: deterministic parametric glyph generator
|
const charset = [
|
||||||
// This is where future AI (you call me) can map characters -> vector instructions.
|
" ",
|
||||||
function genGlyphPathForChar(ch) {
|
"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 p = new opentype.Path();
|
||||||
const code = ch.charCodeAt(0);
|
const m = 60;
|
||||||
|
p.moveTo(m, METRICS.descender);
|
||||||
const seed = (code * 73) % 997;
|
p.lineTo(w - m, METRICS.descender);
|
||||||
const thickness = 40 + (seed % 60);
|
p.lineTo(w - m, METRICS.ascender);
|
||||||
const inset = 80 + (seed % 120);
|
p.lineTo(m, METRICS.ascender);
|
||||||
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();
|
p.close();
|
||||||
|
p.moveTo(m + 60, METRICS.descender + 60);
|
||||||
// Top stain-like slab
|
p.lineTo(w - m - 60, METRICS.descender + 60);
|
||||||
const slabW = 260 + (seed % 140);
|
p.lineTo(w - m - 60, METRICS.ascender - 60);
|
||||||
const slabH = 80 + (seed % 50);
|
p.lineTo(m + 60, METRICS.ascender - 60);
|
||||||
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();
|
p.close();
|
||||||
|
return new opentype.Glyph({
|
||||||
// Bottom irregular notch
|
name: ".notdef",
|
||||||
const notchW = 120 + (seed % 80);
|
unicode: 0,
|
||||||
const notchY = descent + 40 + (seed % 60);
|
advanceWidth: w,
|
||||||
p.moveTo(cx - notchW / 2, notchY);
|
path: p
|
||||||
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) {
|
function createGlyph(ch) {
|
||||||
const unicode = ch.charCodeAt(0);
|
const unicode = ch.codePointAt(0);
|
||||||
const pathObj = genGlyphPathForChar(ch);
|
const { path, width } = buildGlyph(ch);
|
||||||
return new opentype.Glyph({
|
return new opentype.Glyph({
|
||||||
name: ch === " " ? "space" : `uni${unicode.toString(16).toUpperCase()}`,
|
name: ch === " " ? "space" : `uni${unicode.toString(16).toUpperCase()}`,
|
||||||
unicode,
|
unicode,
|
||||||
advanceWidth,
|
advanceWidth: width,
|
||||||
path: pathObj
|
path
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFont() {
|
function buildFont() {
|
||||||
const fontName = "StainFont Basic";
|
const glyphs = [buildNotdef()];
|
||||||
const chars = [
|
charset.forEach(ch => glyphs.push(createGlyph(ch)));
|
||||||
" ",
|
|
||||||
"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({
|
const font = new opentype.Font({
|
||||||
familyName: fontName,
|
familyName: FAMILY,
|
||||||
styleName: "Regular",
|
styleName: STYLE,
|
||||||
unitsPerEm: upem,
|
unitsPerEm: METRICS.unitsPerEm,
|
||||||
ascender: ascent,
|
ascender: METRICS.ascender,
|
||||||
descender: descent,
|
descender: METRICS.descender,
|
||||||
glyphs
|
glyphs
|
||||||
});
|
});
|
||||||
|
|
||||||
const ttfBuffer = Buffer.from(font.toArrayBuffer());
|
const ttf = Buffer.from(font.toArrayBuffer());
|
||||||
|
const out = path.join(distDir, "Stain-Regular.ttf");
|
||||||
const outTTF = path.join(distDir, "StainFont-Basic.ttf");
|
fs.writeFileSync(out, ttf);
|
||||||
fs.writeFileSync(outTTF, ttfBuffer);
|
console.log(`Built: ${out}`);
|
||||||
|
|
||||||
console.log(`Built: ${outTTF}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFont();
|
buildFont();
|
||||||
|
|||||||
Reference in New Issue
Block a user