Files
ark-stronghold-protocol/tools/assets/fonts.mjs
T
sganggs 01a44cd288 卫戍协议:盟约 web remake — initial import
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).
2026-09-29 16:21:31 +08:00

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 };
}