import { outline } from './iocgn-shapes.mjs'; import config from './iocgn-config.mjs'; function strokePath(points, width) { const normals = points.slice(1).map(([x, y], i) => { const dx = x - points[i][0], dy = y - points[i][1]; const length = Math.hypot(dx, dy); return [-dy / length, dx / length]; }); const offsets = [normals[0].map(v => v * width / 2)]; for (let i = 1; i < normals.length; i++) { const [ax, ay] = normals[i - 1], [bx, by] = normals[i]; const scale = width / 2 / (1 + ax * bx + ay * by); offsets.push([(ax + bx) * scale, (ay + by) * scale]); } offsets.push(normals.at(-1).map(v => v * width / 2)); return [...points.map(([x, y], i) => [x + offsets[i][0], y + offsets[i][1]]), ...points.map(([x, y], i) => [x - offsets[i][0], y - offsets[i][1]]).reverse()]; } function arrowHead(x, y, degrees, width, length = 100) { const angle = degrees * Math.PI / 180, dx = Math.cos(angle), dy = Math.sin(angle); return strokePath([[x - length * dx - length * dy, y - length * dy + length * dx], [x, y], [x - length * dx + length * dy, y - length * dy - length * dx]], width); } function arcPoints(cx, cy, radius, from, to, steps) { return Array.from({ length: steps + 1 }, (_, i) => { const angle = (from + (to - from) * i / steps) * Math.PI / 180; return [cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)]; }); } export function refineArrows(store) { // Preserve the approved stroke weight, including native TrueType rounding. const ys = store.queryByUnicodeEnsured(45).geometry.toContours().flat().map(p => Math.round(p.y)); const width = (Math.max(...ys) - Math.min(...ys)) * .72; const shapes = { '→': [strokePath([[70, 340], [415, 340]], width), arrowHead(420, 340, 0, width)], '↔': [strokePath([[85, 340], [415, 340]], width), arrowHead(80, 340, 180, width, 90), arrowHead(420, 340, 0, width, 90)], '⇒': [...[292, 388].map(y => strokePath([[75, y], [372, y]], width * .75)), arrowHead(420, 340, 0, width * .75, 112)], '⇔': [...[298, 382].map(y => strokePath([[125, y], [375, y]], width * .75)), arrowHead(80, 340, 180, width * .75, 90), arrowHead(420, 340, 0, width * .75, 90)], '⇥': [strokePath([[65, 340], [390, 340]], width), arrowHead(395, 340, 0, width, 95), strokePath([[460, 200], [460, 480]], width * .55)], '➜': [strokePath([[65, 340], [400, 340]], width * 1.15), arrowHead(410, 340, 0, width * 1.15, 105)], '❯': [strokePath([[185, 470], [315, 340], [185, 210]], width * 1.15)], '↩': [strokePath([[80, 245], ...arcPoints(345, 325, 80, -90, 90, 20), [95, 405]], width), arrowHead(90, 405, 180, width, 90)], '↴': [strokePath([[70, 500], [325, 500], [325, 180]], width), arrowHead(325, 175, -90, width, 90)], '↵': [strokePath([[360, 590], [360, 220], [95, 220]], width), arrowHead(90, 220, 180, width, 90)], }; const circle = arcPoints(250, 340, 165, -15, -315, 40); shapes['↻'] = [strokePath(circle, width * .85), arrowHead(...circle.at(-1), -45, width * .85, 75)]; for (const [char, y, sign, start] of [['↰', 440, 1, 140], ['↲', 280, -1, 590]]) { shapes[char] = [strokePath([[360, start], ...arcPoints(300, y, 60, 0, sign * 90, 12), [95, y + sign * 60]], width), arrowHead(90, y + sign * 60, 180, width, 90)]; } for (const [char, source, degrees] of [ ['←', '→', 180], ['↑', '→', 90], ['↓', '→', -90], ['↖', '→', 135], ['↗', '→', 45], ['↘', '→', -45], ['↙', '→', -135], ['↕', '↔', 90], ['⇐', '⇒', 180], ['⇑', '⇒', 90], ['⇓', '⇒', -90], ]) { const angle = degrees * Math.PI / 180, c = Math.cos(angle), s = Math.sin(angle); shapes[char] = shapes[source].map(contour => contour.map(([x, y]) => [250 + (x - 250) * c - (y - 340) * s, 340 + (x - 250) * s + (y - 340) * c])); } for (const [char, source] of [['⇤', '⇥'], ['❮', '❯'], ['↺', '↻'], ['↪', '↩'], ['↱', '↰'], ['↳', '↲']]) { shapes[char] = shapes[source].map(contour => contour.map(([x, y]) => [500 - x, y])); } for (const char of config.terminal.arrows) { const contours = shapes[char], points = contours.flat(); const xs = points.map(p => p[0]), ys = points.map(p => p[1]); const x0 = Math.min(...xs), x1 = Math.max(...xs), y0 = Math.min(...ys), y1 = Math.max(...ys); // Uniform scaling keeps arrowheads undistorted; vertical arrows are rotated peers. const scale = 500 / ('↑↓↕⇑⇓'.includes(char) ? y1 - y0 : x1 - x0); const glyph = store.queryByUnicodeEnsured(char.codePointAt(0)); glyph.koelnPreserveTopology = true; glyph.clearGeometry(); outline(contours.map(contour => contour.map(([x, y]) => [250 + scale * (x - (x0 + x1) / 2), 340 + scale * (y - (y0 + y1) / 2)]))).applyToGlyph(glyph); } }