Feat: Build minimal Stain font to dist

This commit is contained in:
2025-11-09 08:40:52 -08:00
parent ba2d01bb8b
commit f4581954ac

184
scripts/build-font.mjs Normal file
View File

@@ -0,0 +1,184 @@
import fs from "fs";
import path from "path";
import url from "url";
import opentype from "opentype.js";
const __dirname=url.fileURLToPath(new URL(".",import.meta.url));
const distDir=path.resolve(__dirname,"..","dist");
if(!fs.existsSync(distDir))fs.mkdirSync(distDir,{recursive:true});
const upm=1000;
const ascent=800;
const descent=-200;
const bw=80;
const gap=40;
const makePath=d=>new opentype.Path(d);
const glyph=({name,unicode,advanceWidth,commands})=>new opentype.Glyph({
name,
unicode,
advanceWidth,
path:makePath(commands)
});
// Simple Candara-inspired, clean humanist-ish shapes
const glyphs=[];
// space
glyphs.push(glyph({
name:"space",
unicode:32,
advanceWidth:300,
commands:[]
}));
// Helper for vertical stems
const vStem=(x,y1,y2,w)=>[
{type:"M",x:x,y:y1},
{type:"L",x:x+w,y:y1},
{type:"L",x:x+w,y:y2},
{type:"L",x:x,y:y2},
{type:"Z"}
];
// Helper for horizontal bar
const hBar=(x1,x2,y,w)=>[
{type:"M",x:x1,y:y},
{type:"L",x:x2,y:y},
{type:"L",x:x2,y:y+w},
{type:"L",x:x1,y:y+w},
{type:"Z"}
];
// Uppercase A
(()=>{
const aw=600;
const base=0,top=ascent;
const left=140,right=460;
const barY=450,barW=60;
const cmds=[
{type:"M",x:80,y:base},
{type:"L",x:200,y:top},
{type:"L",x:260,y:top},
{type:"L",x:480,y:base},
{type:"L",x:400,y:base},
{type:"L",x:250,y:620},
{type:"L",x:120,y:base},
{type:"Z"},
...hBar(left,right,barY,barW)
];
glyphs.push(glyph({name:"A",unicode:65,advanceWidth:aw,commands:cmds}));
})();
// Uppercase B
(()=>{
const aw=600;
const x0=90;
const mid=450;
const r=230;
const cmds=[
...vStem(x0,0,ascent,bw),
{type:"M",x:x0+bw,y:ascent},
{type:"Q",x:x0+bw+220,y:ascent,x1:x0+bw+220,y1:ascent-180},
{type:"Q",x:x0+bw+220,y:ascent-320,x1:x0+bw,y1:mid},
{type:"L",x:x0+bw,y:mid},
{type:"Z"},
{type:"M",x:x0+bw,y:mid},
{type:"Q",x:x0+bw+200,y:mid,x1:x0+bw+200,y1:mid-160},
{type:"Q",x:x0+bw+200,y:mid-310,x1:x0+bw,y1:0},
{type:"L",x:x0+bw,y:0},
{type:"Z"}
];
glyphs.push(glyph({name:"B",unicode:66,advanceWidth:aw,commands:cmds}));
})();
// Uppercase C
(()=>{
const aw=600;
const cx=340,cy=400,ro=320,ri=220;
const cmds=[
{type:"M",x:cx+ro,y:cy+10},
{type:"Q",x:cx+ro-140,y:cy+260,x1:cx,y1:cy+260},
{type:"Q",x:cx-ro+40,y:cy+260,x1:cx-ro+40,y1:cy},
{type:"Q",x:cx-ro+40,y:cy-260,x1:cx,y1:cy-260},
{type:"Q",x:cx+ro-80,y:cy-260,x1:cx+ro,y1:cy-40},
{type:"L",x:cx+ro-60,y:cy-40},
{type:"Q",x:cx+ro-120,y:cy-210,x1:cx,y1:cy-210},
{type:"Q",x:cx-ri,y:cy-210,x1:cx-ri,y1:cy},
{type:"Q",x:cx-ri,y:cy+210,x1:cx,y1:cy+210},
{type:"Q",x:cx+ro-120,y:cy+210,x1:cx+ro-40,y1:cy+40},
{type:"Z"}
];
glyphs.push(glyph({name:"C",unicode:67,advanceWidth:aw,commands:cmds}));
})();
// Lowercase a
(()=>{
const aw=520;
const base=0,x0=90;
const cmds=[
{type:"M",x:x0+80,y:base},
{type:"Q",x:x0,y:base+70,x1:x0,y1:base+150},
{type:"Q",x:x0,y:base+260,x1:x0+110,y1:base+260},
{type:"Q",x:x0+210,y:base+260,x1:x0+260,y1:base+210},
{type:"L",x:x0+260,y:base},
{type:"L",x:x0+200,y:base},
{type:"L",x:x0+200,y:base+40},
{type:"Q",x:x0+160,y:base,x1:x0+80,y1:base},
{type:"Z"}
];
glyphs.push(glyph({name:"a",unicode:97,advanceWidth:aw,commands:cmds}));
})();
// Lowercase b
(()=>{
const aw=520;
const x0=90;
const top=700;
const mid=350;
const cmds=[
...vStem(x0,0,top,bw),
{type:"M",x:x0+bw,y:mid},
{type:"Q",x:x0+bw+160,y:mid,x1:x0+bw+160,y1:mid-130},
{type:"Q",x:x0+bw+160,y:mid-260,x1:x0+bw,y1:mid-260},
{type:"Q",x:x0+40,y:mid-260,x1:x0,y1:mid-140},
{type:"Q",x:x0,y:mid-40,x1:x0+40,y1:mid},
{type:"Z"}
];
glyphs.push(glyph({name:"b",unicode:98,advanceWidth:aw,commands:cmds}));
})();
// Lowercase c
(()=>{
const aw=480;
const cx=260,cy=260;
const cmds=[
{type:"M",x:cx+160,y:cy+40},
{type:"Q",x:cx+40,y:cy+160,x1:cx-60,y1:cy+160},
{type:"Q",x:cx-150,y:cy+160,x1:cx-150,y1:cy},
{type:"Q",x:cx-150,y:cy-160,x1:cx-40,y1:cy-160},
{type:"Q",x:cx+40,y:cy-160,x1:cx+120,y1:cy-60},
{type:"L",x:cx+80,y:cy-40},
{type:"Q",x:cx+20,y:cy-110,x1:cx-40,y1:cy-110},
{type:"Q",x:cx-80,y:cy-110,x1:cx-80,y1:cy},
{type:"Q",x:cx-80,y:cy+110,x1:cx-20,y1:cy+110},
{type:"Q",x:cx+40,y:cy+110,x1:cx+130,y1:cy+10},
{type:"Z"}
];
glyphs.push(glyph({name:"c",unicode:99,advanceWidth:aw,commands:cmds}));
})();
const font=new opentype.Font({
familyName:"Stain",
styleName:"Regular",
unitsPerEm:upm,
ascender:ascent,
descender:descent,
glyphs
});
const ttf=font.toArrayBuffer();
const outPath=path.join(distDir,"Stain-Regular.ttf");
fs.writeFileSync(outPath,Buffer.from(ttf));
console.log("Built",outPath);