'use strict'; // Extract the glyph outlines the site icons and the og-image wordmark need from a // grotesque font (LiberationSans = Arial-metric, matching the game's system-ui/Arial // stack) and emit cubic-bezier contours per character, baseline at y=0, y-down, // plus the advance width so generate.js can lay out words without the font. // Same outline conversion as ../../vk/build/extract.js, generalised to a char set. 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 // The tile glyphs («Э», «8») + every character of the og-image wordmark lines // («Эрудит», «Скрэббл — игра в слова»). The space carries only an advance. const CHARS = [...new Set('Э8рудитСкэббл—игра в слова')]; function glyphData(ch) { const g = font.charToGlyph(ch); const p = g.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 }; }); const bbox = out.length ? { x: minx, y: miny, w: maxx - minx, h: maxy - miny } : { x: 0, y: 0, w: 0, h: 0 }; // the space has no outline return { adv: g.advanceWidth * (FS / font.unitsPerEm), bbox, contours: out }; } const glyphs = {}; for (const ch of CHARS) glyphs[ch] = glyphData(ch); const out = __dirname + '/glyphs.json'; fs.writeFileSync(out, JSON.stringify({ em: FS, glyphs })); console.log('wrote', out, fs.statSync(out).size, 'bytes;', CHARS.length, 'glyphs:', CHARS.join(''));