char/iocgn-mono

iosevka variant inspired by Simple Köln-Bonn

git clone https://git.t4t.associates/char/iocgn-mono

Charlotte Somkerning tweaksa3bd267

main
5.0 KiB89 linesraw
1import { outline } from './iocgn-shapes.mjs';
2import config from './iocgn-config.mjs';
3
4function strokePath(points, width) {
5    const normals = points.slice(1).map(([x, y], i) => {
6        const dx = x - points[i][0], dy = y - points[i][1];
7        const length = Math.hypot(dx, dy);
8        return [-dy / length, dx / length];
9    });
10    const offsets = [normals[0].map(v => v * width / 2)];
11    for (let i = 1; i < normals.length; i++) {
12        const [ax, ay] = normals[i - 1], [bx, by] = normals[i];
13        const scale = width / 2 / (1 + ax * bx + ay * by);
14        offsets.push([(ax + bx) * scale, (ay + by) * scale]);
15    }
16    offsets.push(normals.at(-1).map(v => v * width / 2));
17    return [...points.map(([x, y], i) => [x + offsets[i][0], y + offsets[i][1]]),
18        ...points.map(([x, y], i) => [x - offsets[i][0], y - offsets[i][1]]).reverse()];
19}
20
21function arrowHead(x, y, degrees, width, length = 100) {
22    const angle = degrees * Math.PI / 180, dx = Math.cos(angle), dy = Math.sin(angle);
23    return strokePath([[x - length * dx - length * dy, y - length * dy + length * dx],
24        [x, y], [x - length * dx + length * dy, y - length * dy - length * dx]], width);
25}
26
27function arcPoints(cx, cy, radius, from, to, steps) {
28    return Array.from({ length: steps + 1 }, (_, i) => {
29        const angle = (from + (to - from) * i / steps) * Math.PI / 180;
30        return [cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)];
31    });
32}
33
34export function refineArrows(store) {
35    // Preserve the approved stroke weight, including native TrueType rounding.
36    const ys = store.queryByUnicodeEnsured(45).geometry.toContours().flat().map(p => Math.round(p.y));
37    const width = (Math.max(...ys) - Math.min(...ys)) * .72;
38    const shapes = {
39        '→': [strokePath([[70, 340], [415, 340]], width), arrowHead(420, 340, 0, width)],
40        '↔': [strokePath([[85, 340], [415, 340]], width),
41            arrowHead(80, 340, 180, width, 90), arrowHead(420, 340, 0, width, 90)],
42        '⇒': [...[292, 388].map(y => strokePath([[75, y], [372, y]], width * .75)),
43            arrowHead(420, 340, 0, width * .75, 112)],
44        '⇔': [...[298, 382].map(y => strokePath([[125, y], [375, y]], width * .75)),
45            arrowHead(80, 340, 180, width * .75, 90), arrowHead(420, 340, 0, width * .75, 90)],
46        '⇥': [strokePath([[65, 340], [390, 340]], width), arrowHead(395, 340, 0, width, 95),
47            strokePath([[460, 200], [460, 480]], width * .55)],
48        '➜': [strokePath([[65, 340], [400, 340]], width * 1.15),
49            arrowHead(410, 340, 0, width * 1.15, 105)],
50        '❯': [strokePath([[185, 470], [315, 340], [185, 210]], width * 1.15)],
51        '↩': [strokePath([[80, 245], ...arcPoints(345, 325, 80, -90, 90, 20), [95, 405]], width),
52            arrowHead(90, 405, 180, width, 90)],
53        '↴': [strokePath([[70, 500], [325, 500], [325, 180]], width),
54            arrowHead(325, 175, -90, width, 90)],
55        '↵': [strokePath([[360, 590], [360, 220], [95, 220]], width),
56            arrowHead(90, 220, 180, width, 90)],
57    };
58    const circle = arcPoints(250, 340, 165, -15, -315, 40);
59    shapes['↻'] = [strokePath(circle, width * .85), arrowHead(...circle.at(-1), -45, width * .85, 75)];
60    for (const [char, y, sign, start] of [['↰', 440, 1, 140], ['↲', 280, -1, 590]]) {
61        shapes[char] = [strokePath([[360, start], ...arcPoints(300, y, 60, 0, sign * 90, 12),
62            [95, y + sign * 60]], width), arrowHead(90, y + sign * 60, 180, width, 90)];
63    }
64    for (const [char, source, degrees] of [
65        ['←', '→', 180], ['↑', '→', 90], ['↓', '→', -90],
66        ['↖', '→', 135], ['↗', '→', 45], ['↘', '→', -45], ['↙', '→', -135],
67        ['↕', '↔', 90], ['⇐', '⇒', 180], ['⇑', '⇒', 90], ['⇓', '⇒', -90],
68    ]) {
69        const angle = degrees * Math.PI / 180, c = Math.cos(angle), s = Math.sin(angle);
70        shapes[char] = shapes[source].map(contour => contour.map(([x, y]) =>
71            [250 + (x - 250) * c - (y - 340) * s, 340 + (x - 250) * s + (y - 340) * c]));
72    }
73    for (const [char, source] of [['⇤', '⇥'], ['❮', '❯'], ['↺', '↻'], ['↪', '↩'], ['↱', '↰'], ['↳', '↲']]) {
74        shapes[char] = shapes[source].map(contour => contour.map(([x, y]) => [500 - x, y]));
75    }
76    for (const char of config.terminal.arrows) {
77        const contours = shapes[char], points = contours.flat();
78        const xs = points.map(p => p[0]), ys = points.map(p => p[1]);
79        const x0 = Math.min(...xs), x1 = Math.max(...xs), y0 = Math.min(...ys), y1 = Math.max(...ys);
80        // Uniform scaling keeps arrowheads undistorted; vertical arrows are rotated peers.
81        const scale = 500 / ('↑↓↕⇑⇓'.includes(char) ? y1 - y0 : x1 - x0);
82        const glyph = store.queryByUnicodeEnsured(char.codePointAt(0));
83        glyph.koelnPreserveTopology = true;
84        glyph.clearGeometry();
85        outline(contours.map(contour => contour.map(([x, y]) =>
86            [250 + scale * (x - (x0 + x1) / 2),
87                340 + scale * (y - (y0 + y1) / 2)]))).applyToGlyph(glyph);
88    }
89}