Online co-op browser remake of Arknights' Stronghold Protocol: Alliance (client-simulated combat, official waves/bonds/items/strategies, 3D board, facing wheel, operator loadout, official emotes, deployment scripts).
87 lines
4.2 KiB
JavaScript
87 lines
4.2 KiB
JavaScript
// Self-hosted UI fonts (research 07 §8): Bender (numerals/HUD) and Novecento
|
|
// Wide (Latin headers), from TimWangZi/The-font-of-Arknights. Downloaded next
|
|
// to the site as public/fonts/*.{otf,ttf}, converted to .woff2 (tools/assets/
|
|
// woff2.mjs) and exposed through public/fonts/fonts.css. Noto Sans/Serif SC are
|
|
// NOT self-hosted (Google Fonts with system fallback, see DESIGN §10).
|
|
|
|
import { readFile, writeFile, mkdir, rename, stat } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
import { RAW, joinUrl } from './sources.mjs';
|
|
import { encodeWoff2, decodeWoff2Tables, readSfnt } from './woff2.mjs';
|
|
|
|
/** Font files: family, weight, source path in the font repo, local base name. */
|
|
export const FONTS = Object.freeze([
|
|
{ family: 'Bender', weight: 400, style: 'normal', src: 'Bender/BENDER.OTF', name: 'bender-regular', ext: 'otf', bytes: 52536 },
|
|
{ family: 'Bender', weight: 300, style: 'normal', src: 'Bender/BenderLight.woff.ttf', name: 'bender-light', ext: 'ttf', bytes: 68256 },
|
|
{ family: 'Novecento Wide', weight: 400, style: 'normal', src: 'Novecento-Wide-Normal-2.otf', name: 'novecento-wide-normal', ext: 'otf', bytes: 45356 },
|
|
]);
|
|
|
|
/**
|
|
* Download jobs for the font files (rel paths are relative to public/fonts).
|
|
* @returns {{rel:string, urls:string[], kind:string, bytes:number}[]}
|
|
*/
|
|
export function fontJobs() {
|
|
return FONTS.map((f) => ({ rel: `${f.name}.${f.ext}`, urls: [joinUrl(RAW.fonts, f.src)], kind: 'font', bytes: f.bytes }));
|
|
}
|
|
|
|
async function exists(p) { try { return (await stat(p)).isFile(); } catch { return false; } }
|
|
|
|
/**
|
|
* Convert downloaded fonts to WOFF2 (skipping up-to-date outputs) and write fonts.css.
|
|
* @param {string} fontsDir absolute public/fonts directory
|
|
* @param {(m:string)=>void} log
|
|
* @returns {Promise<{ files: Record<string, {family:string, weight:number, woff2?:string, original?:string}>, css: string|null, errors: string[] }>}
|
|
*/
|
|
export async function buildFonts(fontsDir, log = console.log) {
|
|
await mkdir(fontsDir, { recursive: true });
|
|
const files = {};
|
|
const errors = [];
|
|
const faces = [];
|
|
for (const f of FONTS) {
|
|
const orig = join(fontsDir, `${f.name}.${f.ext}`);
|
|
const woff2 = join(fontsDir, `${f.name}.woff2`);
|
|
if (!(await exists(orig))) { errors.push(`missing font ${f.name}.${f.ext}`); continue; }
|
|
const entry = { family: f.family, weight: f.weight, original: `/fonts/${f.name}.${f.ext}` };
|
|
try {
|
|
const src = await readFile(orig);
|
|
readSfnt(src); // validate
|
|
let ok = false;
|
|
if (await exists(woff2)) {
|
|
// Up to date when it decodes to exactly the source tables.
|
|
try {
|
|
const dec = decodeWoff2Tables(await readFile(woff2));
|
|
const s = readSfnt(src);
|
|
ok = dec.tables.length === s.tables.length && s.tables.every((t) => dec.tables.find((d) => d.tag === t.tag)?.data.equals(t.data));
|
|
} catch { ok = false; }
|
|
}
|
|
if (!ok) {
|
|
const out = encodeWoff2(src);
|
|
const tmp = woff2 + '.tmp';
|
|
await writeFile(tmp, out);
|
|
await rename(tmp, woff2);
|
|
log(`[fonts] ${f.name}.woff2 ${src.length} → ${out.length} B`);
|
|
}
|
|
entry.woff2 = `/fonts/${f.name}.woff2`;
|
|
} catch (e) {
|
|
errors.push(`${f.name}: ${e.message}`);
|
|
}
|
|
files[f.name] = entry;
|
|
const srcs = [];
|
|
if (entry.woff2) srcs.push(`url('${entry.woff2}') format('woff2')`);
|
|
srcs.push(`url('${entry.original}') format('${f.ext === 'ttf' ? 'truetype' : 'opentype'}')`);
|
|
faces.push(`@font-face {\n font-family: '${f.family}';\n font-style: ${f.style};\n font-weight: ${f.weight};\n` +
|
|
` font-display: swap;\n src: ${srcs.join(',\n ')};\n}`);
|
|
}
|
|
let css = null;
|
|
if (faces.length) {
|
|
css = `/* Generated by tools/fetch-assets.mjs — do not edit.\n` +
|
|
` * Bender (c) Jovanny Lemonad / Oleg Zhuravlev, Gladkikh Ivan — free font.\n` +
|
|
` * Novecento Wide (c) Jan Tonellato / Synthview — free font.\n` +
|
|
` * Usage: font-family: Bender, Rajdhani, 'Noto Sans SC', sans-serif;\n` +
|
|
` * font-family: 'Novecento Wide', 'Saira Condensed', 'Noto Sans SC', sans-serif; */\n\n` +
|
|
faces.join('\n\n') + '\n';
|
|
await writeFile(join(fontsDir, 'fonts.css'), css);
|
|
}
|
|
return { files, css, errors };
|
|
}
|