storebound/cli/shots/caption.html
CRT d942275920 shots: glyph coverage check per language, automatic Noto fallbacks, check errors on fallback / system-font glyphs
Lilita One lacks most of Latin Extended-A (Turkish s-cedilla, g-breve, dotted I ...), so Turkish titles were drawn
with a silent browser fallback while shots check passed. shots fonts now reads the cmap of every face (pure TS,
TTF/OTF/WOFF/WOFF2), adds a Noto fallback per missing character or fails with language + characters; render
measures which face drew each character (CDP probes) and records it in report.json; check errors on fallbacks the
config did not choose (shots.fonts.fallback) and on system-font glyphs. Default game title font: Paytone One.
2026-10-12 03:05:23 +08:00

42 lines
2.2 KiB
HTML

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/fonts/fonts.css">
<!-- Caption overlay for app preview videos (transparent PNG, rendered by `storebound shots video`, overlaid by ffmpeg).
caption.html?w=<px>&h=<px>&lang=<id>&i=<caption index>. Same title font, stroke and colours as the screenshots. -->
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: transparent; overflow: hidden; }
#c { position: relative; width: var(--W); height: var(--H); }
.cap { position: absolute; left: 50%; top: calc(var(--u) * 7); transform: translateX(-50%) rotate(-1.5deg); max-width: calc(var(--u) * 90);
padding: calc(var(--u) * 2.6) calc(var(--u) * 5) calc(var(--u) * 3); border-radius: calc(var(--u) * 5); text-align: center;
background: linear-gradient(180deg, rgba(0,0,0,.42), rgba(0,0,0,.28)); box-shadow: inset 0 0 0 calc(var(--u) * .35) rgba(255,255,255,.3);
font-family: var(--tf); font-weight: var(--tw); font-size: calc(var(--u) * var(--fs)); line-height: 1.04; color: #fff;
-webkit-text-stroke: calc(var(--u) * var(--fs) * .07) var(--ink); paint-order: stroke fill; text-wrap: balance;
filter: drop-shadow(0 calc(var(--u) * .7) 0 var(--ink)); }
</style>
</head>
<body>
<div id="c"></div>
<script type="module">
const q = new URLSearchParams(location.search);
const W = +q.get('w'), H = +q.get('h'), LANG = q.get('lang'), I = +q.get('i');
const job = await (await fetch(`/caption.json?lang=${encodeURIComponent(LANG)}&i=${I}`)).json();
const u = Math.min(W, H) / 100, R = document.documentElement.style;
document.documentElement.lang = LANG;
R.setProperty('--W', W + 'px'); R.setProperty('--H', H + 'px'); R.setProperty('--u', u + 'px');
R.setProperty('--ink', job.ink); R.setProperty('--fs', W > H ? 6.4 : 8.2);
const X = job.fonts.script;
R.setProperty('--tf', (job.fonts.titleStack ?? (X ? [X, job.fonts.title] : [job.fonts.title])).map(f => `'${f}'`).join(', ') + ', sans-serif');
R.setProperty('--tw', X ? 900 : 400);
const el = document.createElement('div');
el.className = 'cap';
if (job.rtl) el.dir = 'rtl';
el.textContent = job.text;
document.getElementById('c').append(el);
await document.fonts.ready;
document.body.dataset.ready = '1';
</script>
</body>
</html>