diff --git a/scripts/build-font.mjs b/scripts/build-font.mjs new file mode 100644 index 0000000..7ad9b1b --- /dev/null +++ b/scripts/build-font.mjs @@ -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);