mirror of
https://github.com/multipleof4/stain.otf.git
synced 2026-01-13 16:17:55 +00:00
Feat: Build minimal Stain font to dist
This commit is contained in:
184
scripts/build-font.mjs
Normal file
184
scripts/build-font.mjs
Normal 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);
|
||||
Reference in New Issue
Block a user