mirror of
https://github.com/multipleof4/stain.otf.git
synced 2026-01-13 16:17:55 +00:00
Feat: Generate Stain font ttf in dist
This commit is contained in:
290
scripts/build-font.js
Normal file
290
scripts/build-font.js
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import url from "url";
|
||||||
|
import opentype from "opentype.js";
|
||||||
|
|
||||||
|
const __filename = url.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 UPM = 1000;
|
||||||
|
const ASC = 750;
|
||||||
|
const DSC = -250;
|
||||||
|
const STROKE = 72;
|
||||||
|
const CURVE = 0.44;
|
||||||
|
const LIGHT = 0.085;
|
||||||
|
const ANG = 9 * Math.PI / 180;
|
||||||
|
|
||||||
|
const makeGlyph = ({
|
||||||
|
name,
|
||||||
|
unicode,
|
||||||
|
advanceWidth,
|
||||||
|
path: makePath
|
||||||
|
}) => new opentype.Glyph({
|
||||||
|
name,
|
||||||
|
unicode,
|
||||||
|
advanceWidth,
|
||||||
|
path: makePath()
|
||||||
|
});
|
||||||
|
|
||||||
|
const slant = x => x + Math.tan(ANG) * 0.12 * UPM;
|
||||||
|
const mix = (a, b, t) => a + (b - a) * t;
|
||||||
|
const curvePt = (x1, y1, x2, y2, t) => ({
|
||||||
|
x: mix(x1, x2, t),
|
||||||
|
y: mix(y1, y2, t)
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseMetrics = {
|
||||||
|
ascender: ASC,
|
||||||
|
descender: DSC,
|
||||||
|
unitsPerEm: UPM
|
||||||
|
};
|
||||||
|
|
||||||
|
const stainCommon = {
|
||||||
|
familyName: "Stain",
|
||||||
|
styleName: "Regular",
|
||||||
|
...baseMetrics
|
||||||
|
};
|
||||||
|
|
||||||
|
const pA = () => {
|
||||||
|
const p = new opentype.Path();
|
||||||
|
const w = 640;
|
||||||
|
const barH = 320;
|
||||||
|
const apexX = slant(120);
|
||||||
|
const apexY = ASC;
|
||||||
|
const leftBaseX = slant(80);
|
||||||
|
const rightBaseX = slant(w - 40);
|
||||||
|
const baseY = DSC * LIGHT;
|
||||||
|
p.moveTo(rightBaseX, baseY);
|
||||||
|
p.lineTo(rightBaseX - STROKE * 0.32, baseY);
|
||||||
|
p.lineTo(apexX + 36, apexY);
|
||||||
|
p.quadraticCurveTo(apexX, apexY - 18, apexX - 36, apexY);
|
||||||
|
p.lineTo(leftBaseX + STROKE * 0.28, baseY);
|
||||||
|
p.lineTo(leftBaseX, baseY);
|
||||||
|
p.close();
|
||||||
|
const ib = 0.35;
|
||||||
|
const iLeft = mix(leftBaseX, rightBaseX, ib);
|
||||||
|
const iRight = mix(leftBaseX, rightBaseX, ib + 0.36);
|
||||||
|
const iTop = barH + 22;
|
||||||
|
const iBot = barH - 22;
|
||||||
|
p.moveTo(iLeft, iTop);
|
||||||
|
p.lineTo(iRight, iTop);
|
||||||
|
p.lineTo(iRight, iBot);
|
||||||
|
p.lineTo(iLeft, iBot);
|
||||||
|
p.close();
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pB = () => {
|
||||||
|
const p = new opentype.Path();
|
||||||
|
const xL = slant(80);
|
||||||
|
const xStemR = xL + STROKE * 0.9;
|
||||||
|
const yTop = ASC;
|
||||||
|
const yBot = DSC * LIGHT;
|
||||||
|
const midY = (yTop + yBot) / 2 + 40;
|
||||||
|
const o = 120;
|
||||||
|
const w = 640;
|
||||||
|
const cxR = slant(w - 40);
|
||||||
|
p.moveTo(xStemR, yBot);
|
||||||
|
p.lineTo(xL, yBot);
|
||||||
|
p.lineTo(xL, yTop);
|
||||||
|
p.lineTo(xStemR, yTop);
|
||||||
|
p.close();
|
||||||
|
p.moveTo(xStemR, yTop);
|
||||||
|
p.curveTo(
|
||||||
|
mix(xStemR, cxR, CURVE), yTop,
|
||||||
|
cxR, mix(yTop, midY + 26, CURVE),
|
||||||
|
cxR, midY + 26
|
||||||
|
);
|
||||||
|
p.curveTo(
|
||||||
|
cxR, mix(midY + 26, midY - 8, CURVE),
|
||||||
|
mix(xStemR + o, cxR, CURVE), midY - 8,
|
||||||
|
xStemR + o, midY - 8
|
||||||
|
);
|
||||||
|
p.lineTo(xStemR, midY - 8);
|
||||||
|
p.close();
|
||||||
|
p.moveTo(xStemR, midY - 26);
|
||||||
|
p.curveTo(
|
||||||
|
mix(xStemR, cxR, CURVE), midY - 26,
|
||||||
|
cxR, mix(midY - 26, yBot + 40, CURVE),
|
||||||
|
cxR, yBot + 40
|
||||||
|
);
|
||||||
|
p.curveTo(
|
||||||
|
cxR, mix(yBot + 40, yBot + 8, CURVE),
|
||||||
|
mix(xStemR + o, cxR, CURVE), yBot + 8,
|
||||||
|
xStemR + o, yBot + 8
|
||||||
|
);
|
||||||
|
p.lineTo(xStemR, yBot + 8);
|
||||||
|
p.close();
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pC = () => {
|
||||||
|
const p = new opentype.Path();
|
||||||
|
const cx = slant(360);
|
||||||
|
const cy = ASC * 0.55;
|
||||||
|
const rx = 290;
|
||||||
|
const ry = 260;
|
||||||
|
const i = 0.56;
|
||||||
|
const rxI = rx * i;
|
||||||
|
const ryI = ry * i;
|
||||||
|
const startAng = Math.PI * 0.22;
|
||||||
|
const endAng = Math.PI * 1.78;
|
||||||
|
const arc = (R, rY, rev) => {
|
||||||
|
const dir = rev ? -1 : 1;
|
||||||
|
const steps = 6;
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const t = i / steps;
|
||||||
|
const a = mix(startAng, endAng, t);
|
||||||
|
const x = cx + Math.cos(a) * R;
|
||||||
|
const y = cy + Math.sin(a) * rY;
|
||||||
|
if (i === 0 && !rev) p.moveTo(x, y);
|
||||||
|
else p.lineTo(x, y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
arc(rx, ry, false);
|
||||||
|
arc(rxI, ryI, true);
|
||||||
|
p.close();
|
||||||
|
const cutW = 80;
|
||||||
|
const cutX = cx + rx * 0.76;
|
||||||
|
const cutY1 = cy + 40;
|
||||||
|
const cutY2 = cy - 40;
|
||||||
|
p.moveTo(cutX, cutY1);
|
||||||
|
p.lineTo(cutX + cutW, cutY1 + 16);
|
||||||
|
p.lineTo(cutX + cutW, cutY2 - 16);
|
||||||
|
p.lineTo(cutX, cutY2);
|
||||||
|
p.close();
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pa = () => {
|
||||||
|
const p = new opentype.Path();
|
||||||
|
const x = slant(120);
|
||||||
|
const yBase = 0;
|
||||||
|
const h = 500;
|
||||||
|
const rx = 180;
|
||||||
|
const ry = 210;
|
||||||
|
const cx = slant(260);
|
||||||
|
const cy = h * 0.54;
|
||||||
|
const steps = 5;
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const t = i / steps;
|
||||||
|
const a = Math.PI * (0.15 + 1.85 * t);
|
||||||
|
const xx = cx + Math.cos(a) * rx;
|
||||||
|
const yy = cy + Math.sin(a) * ry;
|
||||||
|
if (!i) p.moveTo(xx, yy);
|
||||||
|
else p.lineTo(xx, yy);
|
||||||
|
}
|
||||||
|
const rxI = rx * 0.56;
|
||||||
|
const ryI = ry * 0.56;
|
||||||
|
for (let i = steps; i >= 0; i--) {
|
||||||
|
const t = i / steps;
|
||||||
|
const a = Math.PI * (0.18 + 1.6 * t);
|
||||||
|
const xx = cx + Math.cos(a) * rxI;
|
||||||
|
const yy = cy + Math.sin(a) * ryI;
|
||||||
|
p.lineTo(xx, yy);
|
||||||
|
}
|
||||||
|
p.close();
|
||||||
|
const stemX = slant(360);
|
||||||
|
const top = h + 14;
|
||||||
|
p.moveTo(stemX, yBase - 10);
|
||||||
|
p.lineTo(stemX + STROKE * 0.7, yBase);
|
||||||
|
p.lineTo(stemX + STROKE * 0.7, top);
|
||||||
|
p.lineTo(stemX, top - 14);
|
||||||
|
p.close();
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pb = () => {
|
||||||
|
const p = new opentype.Path();
|
||||||
|
const x = slant(120);
|
||||||
|
const yBase = 0;
|
||||||
|
const top = ASC;
|
||||||
|
p.moveTo(x, yBase - 10);
|
||||||
|
p.lineTo(x + STROKE * 0.8, yBase);
|
||||||
|
p.lineTo(x + STROKE * 0.8, top);
|
||||||
|
p.lineTo(x, top - 16);
|
||||||
|
p.close();
|
||||||
|
const cx = slant(340);
|
||||||
|
const cy = 360;
|
||||||
|
const rx = 180;
|
||||||
|
const ry = 210;
|
||||||
|
const steps = 6;
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const t = i / steps;
|
||||||
|
const a = Math.PI * (0.1 + 1.9 * t);
|
||||||
|
const xx = cx + Math.cos(a) * rx;
|
||||||
|
const yy = cy + Math.sin(a) * ry;
|
||||||
|
if (!i) p.moveTo(xx, yy);
|
||||||
|
else p.lineTo(xx, yy);
|
||||||
|
}
|
||||||
|
const rxI = rx * 0.55;
|
||||||
|
const ryI = ry * 0.55;
|
||||||
|
for (let i = steps; i >= 0; i--) {
|
||||||
|
const t = i / steps;
|
||||||
|
const a = Math.PI * (0.2 + 1.6 * t);
|
||||||
|
const xx = cx + Math.cos(a) * rxI;
|
||||||
|
const yy = cy + Math.sin(a) * ryI;
|
||||||
|
p.lineTo(xx, yy);
|
||||||
|
}
|
||||||
|
p.close();
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pc = () => {
|
||||||
|
const p = new opentype.Path();
|
||||||
|
const cx = slant(260);
|
||||||
|
const cy = 320;
|
||||||
|
const rx = 210;
|
||||||
|
const ry = 210;
|
||||||
|
const rxI = rx * 0.56;
|
||||||
|
const ryI = ry * 0.56;
|
||||||
|
const steps = 6;
|
||||||
|
const s = Math.PI * 0.18;
|
||||||
|
const e = Math.PI * 1.82;
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const t = i / steps;
|
||||||
|
const a = mix(s, e, t);
|
||||||
|
const x = cx + Math.cos(a) * rx;
|
||||||
|
const y = cy + Math.sin(a) * ry;
|
||||||
|
if (!i) p.moveTo(x, y);
|
||||||
|
else p.lineTo(x, y);
|
||||||
|
}
|
||||||
|
for (let i = steps; i >= 0; i--) {
|
||||||
|
const t = i / steps;
|
||||||
|
const a = mix(s + 0.08, e - 0.22, t);
|
||||||
|
const x = cx + Math.cos(a) * rxI;
|
||||||
|
const y = cy + Math.sin(a) * ryI;
|
||||||
|
p.lineTo(x, y);
|
||||||
|
}
|
||||||
|
p.close();
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
const glyphs = [
|
||||||
|
new opentype.Glyph({ name: ".notdef", advanceWidth: 600, path: new opentype.Path() }),
|
||||||
|
new opentype.Glyph({ name: "space", unicode: 32, advanceWidth: 260, path: new opentype.Path() }),
|
||||||
|
makeGlyph({ name: "A", unicode: 65, advanceWidth: 640, path: pA }),
|
||||||
|
makeGlyph({ name: "B", unicode: 66, advanceWidth: 640, path: pB }),
|
||||||
|
makeGlyph({ name: "C", unicode: 67, advanceWidth: 640, path: pC }),
|
||||||
|
makeGlyph({ name: "a", unicode: 97, advanceWidth: 540, path: pa }),
|
||||||
|
makeGlyph({ name: "b", unicode: 98, advanceWidth: 540, path: pb }),
|
||||||
|
makeGlyph({ name: "c", unicode: 99, advanceWidth: 520, path: pc })
|
||||||
|
];
|
||||||
|
|
||||||
|
const font = new opentype.Font({
|
||||||
|
...stainCommon,
|
||||||
|
glyphs,
|
||||||
|
version: "0.3",
|
||||||
|
designer: "multipleof4",
|
||||||
|
license: "OFL-1.1",
|
||||||
|
manufacturer: "multipleof4",
|
||||||
|
description: "Stain: a Candara-inspired humanist sans prototype.",
|
||||||
|
trademark: "Stain"
|
||||||
|
});
|
||||||
|
|
||||||
|
const outPath = path.join(distDir, "Stain-Regular.ttf");
|
||||||
|
const buf = Buffer.from(font.toArrayBuffer());
|
||||||
|
fs.writeFileSync(outPath, buf);
|
||||||
|
console.log("Built", outPath);
|
||||||
Reference in New Issue
Block a user