d0f860a33e
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m30s
A 96x96 transparent Lottie for the VK app loading screen: the «Э» tile (score 8) drops in under gravity, lands with a soft cushion squash (left/right edges bulge convex, corners follow via a chordal spline), springs back and loops (~1.1s). A warm glint is caught on the bounce. Pure 2D shapes (ddd:0) for player compatibility; ~11KB (<24KB limit). Glyphs are real LiberationSans outlines (Arial-metric, matching the game's font stack). Includes a reproducible, dependency-light build pipeline (build/generate.js + extract.js + glyphs.json) and a README. The preview GIF's green-baize background is preview-only; the asset itself is transparent.
68 lines
2.8 KiB
JavaScript
68 lines
2.8 KiB
JavaScript
'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));
|