Letter outlines from strokes
64-bitThe outline of the text: each stroke segment as a rectangle of the pen's width, each stroke point as a disc.
r = height × weight ÷ 2; rectangle corners = a ∓ r·n̂, b ∓ r·n̂; disc corner = c + r·[cos(i·π ÷ 6), sin(i·π ÷ 6)]; line k at y − 1.6·k·height
The polygons are built in 64-bit and handed to the library, which unions them and simplifies the outline with a tolerance of height × 1e-4. The discs are 12-sided with their corners at r, so between corners a joint is narrower than the pen by the factor cos(π ÷ 12).
export function textOutlines(M, text, { height = 1, weight = 0.14 } = {}) {
const r = (height * weight) / 2, polys = [];
const disc = (c) => Array.from({ length: 12 }, (_, i) => [c[0] + r * Math.cos((i * Math.PI) / 6), c[1] + r * Math.sin((i * Math.PI) / 6)]);
String(text).split(/\r?\n/).slice(0, 50).forEach((line, k) => {
const dy = -k * height * 1.6;
for (const pl of textStrokes(line, { h: height }).lines) {
const P = pl.map(([x, y]) => [x, y + dy]);
for (const p of P) polys.push(disc(p));
for (let i = 0; i + 1 < P.length; i++) {
const [a, b] = [P[i], P[i + 1]], dx = b[0] - a[0], dyy = b[1] - a[1], L = Math.hypot(dx, dyy);
if (L < 1e-12) continue;
const nx = (-dyy / L) * r, ny = (dx / L) * r;
// counter-clockwise, so the Positive fill rule unions everything
polys.push([[a[0] - nx, a[1] - ny], [b[0] - nx, b[1] - ny], [b[0] + nx, b[1] + ny], [a[0] + nx, a[1] + ny]]);
}
}
});
const cs = new M.CrossSection(polys, 'Positive');
const clean = cs.simplify(height * 1e-4);
cs.delete?.();
return clean;
}