'use strict'; // Extract the Cyrillic "Э" (U+042D) and the digit "8" outlines from a grotesque // font (LiberationSans = Arial-metric, matching the game's system-ui/Arial stack) // and emit Lottie cubic-bezier contours, centred at the origin, y-down. const opentype = require('opentype.js'); const fs = require('fs'); const FONT = process.argv[2] || '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf'; const b = fs.readFileSync(FONT); const font = opentype.parse(b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength)); const FS = 1000; // em scale function glyphContours(ch) { const p = font.charToGlyph(ch).getPath(0, 0, FS); // baseline at y=0, y-down const contours = []; let cur = null, prev = null; for (const c of p.commands) { if (c.type === 'M') { if (cur) contours.push(cur); cur = [{ v: [c.x, c.y], i: [c.x, c.y], o: [c.x, c.y] }]; prev = { x: c.x, y: c.y }; } else if (c.type === 'L') { cur.push({ v: [c.x, c.y], i: [c.x, c.y], o: [c.x, c.y] }); prev = { x: c.x, y: c.y }; } else if (c.type === 'C') { cur[cur.length - 1].o = [c.x1, c.y1]; cur.push({ v: [c.x, c.y], i: [c.x2, c.y2], o: [c.x, c.y] }); prev = { x: c.x, y: c.y }; } else if (c.type === 'Q') { const c1 = [prev.x + 2 / 3 * (c.x1 - prev.x), prev.y + 2 / 3 * (c.y1 - prev.y)]; const c2 = [c.x + 2 / 3 * (c.x1 - c.x), c.y + 2 / 3 * (c.y1 - c.y)]; cur[cur.length - 1].o = c1; cur.push({ v: [c.x, c.y], i: c2, o: [c.x, c.y] }); prev = { x: c.x, y: c.y }; } else if (c.type === 'Z') { if (cur && cur.length > 1) { const last = cur[cur.length - 1], first = cur[0]; if (Math.hypot(last.v[0] - first.v[0], last.v[1] - first.v[1]) < 1e-3) { first.i = last.i; // fold the duplicate closing point into the first cur.pop(); } } if (cur) { contours.push(cur); cur = null; } } } if (cur) contours.push(cur); let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity; const out = contours.map(ct => { const v = [], i = [], o = []; ct.forEach(pt => { v.push(pt.v); i.push([pt.i[0] - pt.v[0], pt.i[1] - pt.v[1]]); o.push([pt.o[0] - pt.v[0], pt.o[1] - pt.v[1]]); minx = Math.min(minx, pt.v[0]); maxx = Math.max(maxx, pt.v[0]); miny = Math.min(miny, pt.v[1]); maxy = Math.max(maxy, pt.v[1]); }); return { i, o, v, c: true }; }); return { contours: out, bbox: { x: minx, y: miny, w: maxx - minx, h: maxy - miny } }; } const E = glyphContours('Э'); const D8 = glyphContours('8'); fs.writeFileSync('glyphs.json', JSON.stringify({ em: FS, E, D8 })); console.log('Э bbox', E.bbox, 'contours', E.contours.map(c => c.v.length)); console.log('8 bbox', D8.bbox, 'contours', D8.contours.map(c => c.v.length));