Files
stain.otf/scripts/build-font.js

291 lines
7.1 KiB
JavaScript

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);