import { ContourSetGeometry, TransformedGeometry } from '@iosevka/geometry'; import { Point } from '@iosevka/geometry/point'; // Two coordinates describe an on-curve point; six describe a cubic segment. export function outline(contours) { const points = contours.map(contour => contour.flatMap(segment => { if (segment.length === 2) return [Point.corner(...segment)]; return [new Point(Point.Type.CubicStart, segment[0], segment[1]), new Point(Point.Type.CubicEnd, segment[2], segment[3]), Point.corner(segment[4], segment[5])]; })); return { applyToGlyph(glyph) { glyph.includeGeometry(TransformedGeometry.create(glyph.gizmo, new ContourSetGeometry(points))); }, }; } export function arc(cx, cy, radius, from, to) { const count = Math.ceil(Math.abs(to - from) / (Math.PI / 2)); const segments = []; for (let i = 0; i < count; i++) { const a = from + (to - from) * i / count; const b = from + (to - from) * (i + 1) / count; const k = 4 / 3 * Math.tan((b - a) / 4); segments.push([ cx + radius * (Math.cos(a) - k * Math.sin(a)), cy + radius * (Math.sin(a) + k * Math.cos(a)), cx + radius * (Math.cos(b) + k * Math.sin(b)), cy + radius * (Math.sin(b) - k * Math.cos(b)), cx + radius * Math.cos(b), cy + radius * Math.sin(b), ]); } return segments; } // An annular arc gives a constant-width hook even when the terminal stops // near the bottom of the turn. The upstream hook interpolator assumes a // longer, vertical terminal and folds over on these shorter cuts. export function roundHook(right, stemEnd, bottom, radius, stroke, degrees, inverted = false) { radius = Math.max(radius, stroke * 1.2); const cx = right - radius, cy = bottom + radius; const angle = -degrees * Math.PI / 180; const inner = radius - stroke; let contour = [ [right, stemEnd], [right, cy], ...arc(cx, cy, radius, 0, angle), [cx + inner * Math.cos(angle), cy + inner * Math.sin(angle)], ...arc(cx, cy, inner, angle, 0), [right - stroke, stemEnd], ]; if (inverted) contour = contour.map(segment => segment.map(v => -v)); return outline([contour]); } export function capsule(l, r, b, t, radius, reverse = false) { const contour = [[l, b + radius], [l, t - radius], ...arc(l + radius, t - radius, radius, Math.PI, Math.PI / 2), [r - radius, t], ...arc(r - radius, t - radius, radius, Math.PI / 2, 0), [r, b + radius], ...arc(r - radius, b + radius, radius, 0, -Math.PI / 2), [l + radius, b], ...arc(l + radius, b + radius, radius, -Math.PI / 2, -Math.PI)]; return reverse ? reverseContour(contour) : contour; } export function reverseContour(contour) { const reversed = [contour.at(-1).slice(-2)]; for (let i = contour.length - 1; i > 0; i--) { const s = contour[i], end = contour[i - 1].slice(-2); reversed.push(s.length === 2 ? end : [s[2], s[3], s[0], s[1], ...end]); } return reversed; } export function lowerWidth(advance, stroke) { return (319 + .56 * stroke) * advance / 660; } export function lowerBowl(middle, advance, height, stroke, side = 0, stemTop = height, stemBottom = 0) { const span = lowerWidth(advance, stroke), l = middle - span / 2, r = middle + span / 2; const b = -(14.5 - .04 * stroke), t = height + 10, radius = (span - 20 * advance / 660) / 2; const contours = [capsule(l, r, b, t, radius), capsule(l + stroke, r - stroke, b + stroke, t - stroke, radius - stroke, true)]; if (side) { const x = side < 0 ? l : r - stroke; contours.push([[x, stemBottom], [x, stemTop], [x + stroke, stemTop], [x + stroke, stemBottom]]); } return outline(contours); } export function shoulder(middle, advance, height, stemTop, stroke, open = false) { const span = lowerWidth(advance, stroke); const l = middle - (open ? (200 + 1.12 * stroke) * advance / 660 : span) / 2, r = l + span; const radius = open ? (143 + .54 * stroke) * advance / 660 : (span - 20 * advance / 660) / 2; const t = height + 10, cy = t - radius, cx = l + radius, inner = radius - stroke; const end = Math.max(45, Math.min(65, 67.6 - .158 * stroke)) * Math.PI / 180; const arch = [[l, 0], [l, cy], ...arc(cx, cy, radius, Math.PI, Math.PI / 2)]; if (open) { // Extend the shoulder without widening the stem or stretching its curved terminal. const reach = 40 * advance / 660; arch.push([cx + reach, t], ...arc(cx + reach, cy, radius, Math.PI / 2, end), [cx + reach + inner * Math.cos(end), cy + inner * Math.sin(end)], ...arc(cx + reach, cy, inner, end, Math.PI / 2), [cx, t - stroke], ...arc(cx, cy, inner, Math.PI / 2, Math.PI)); } else { arch.push([r - radius, t], ...arc(r - radius, cy, radius, Math.PI / 2, 0), [r, 0], [r - stroke, 0], [r - stroke, cy], ...arc(r - radius, cy, inner, 0, Math.PI / 2), [cx, t - stroke], ...arc(cx, cy, inner, Math.PI / 2, Math.PI)); } arch.push([l + stroke, 0]); return outline([arch, [[l, 0], [l, stemTop], [l + stroke, stemTop], [l + stroke, 0]]]); } export function sixShape(middle, advance, top, stroke) { const span = (368 + .5 * stroke) * advance / 660, l = middle - span / 2, r = middle + span / 2; const radius = (span - 20 * advance / 660) / 2, inner = radius - stroke; const b = -(14.5 - .04 * stroke), t = top - b; const bowlBottom = top * (281.3 / 735) - .38 * stroke; const cy = b + radius, cx = r - radius, cxLeft = cx - 10 * advance / 660; const contours = [capsule(l, r, bowlBottom, t, radius), capsule(l + stroke, r - stroke, bowlBottom + stroke, t - stroke, inner, true), [[r, t - radius], [r, cy], ...arc(cx, cy, radius, 0, -Math.PI / 2), [cxLeft, b], ...arc(cxLeft, cy, radius, -Math.PI / 2, -Math.PI), [cxLeft - inner, cy], ...arc(cxLeft, cy, inner, -Math.PI, -Math.PI / 2), [cx, b + stroke], ...arc(cx, cy, inner, -Math.PI / 2, 0), [r - stroke, t - radius]]]; return outline(contours.map(c => c.map(s => s.map((v, i) => i % 2 ? top - v : 2 * middle - v)))); } export function doubleStoreyA(middle, width, height, stroke) { const s = stroke, span = (351 + .76 * s) * width / 660; const l = middle - span / 2, r = l + lowerWidth(width, s), w = r - l; const b = -(14.5 - .04 * s), t = height + 10; const headLeft = l + 11, radius = (r - headLeft - 21) / 2; const cy = t - radius, cl = headLeft + radius, cr = r - radius, inner = radius - s; const counterTop = height * .652 - .92 * s, floor = b + s; const weight = Math.max(0, Math.min(1, (s - 67) / 38)); const terminal = cy - 15 * weight; const outer = [[headLeft, terminal], [headLeft, cy], ...arc(cl, cy, radius, Math.PI, Math.PI / 2), [cr, t], ...arc(cr, cy, radius, Math.PI / 2, 0), [r, s + 12], [r, s, r + 8, s, r + 12, s], [l + span, s], [l + span, 0], [r, 0, r - .4 * s, 0, r - .8 * s, 33], [r - .40 * w, b, l + .5 * w, b, l + .42 * w, b], [l + .15 * w, b, l, 40, l, 132], [l, 146], [l, 305 + 20 * weight, r - s, 296 + 20 * weight, r - s, cy], ...arc(cr, cy, inner, 0, Math.PI / 2), [cl, t - s], ...arc(cl, cy, inner, Math.PI / 2, Math.PI), [headLeft + s, terminal]]; const counter = [[r - s, counterTop], [r - s - .27 * w, counterTop - 35, l + s, 228, l + s, 146], [l + s, 132], [l + s, floor + 20, l + .33 * w, floor, l + .44 * w, floor], [l + .65 * w, floor, r - s, 116, r - s, 164]]; return outline([outer, counter]); }