storebound/cli/shots/template-bleed.html

97 lines
6.4 KiB
HTML

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/fonts/fonts.css">
<!-- Full-bleed layout (shots.layout "bleed"): the app's own frame fills the whole image, no device and no background,
and a short caption sits in one corner. Legibility comes from a text shadow and a soft glow only: no band, no
scrim, no box behind the words. template-bleed.html?s=<scene>&w=<px>&h=<px>&lang=<id>&size=<size key>.
The frame is read at the size's own pixels: /frames/<lang>/<size>/<src> (shots check: frames for "bleed").
Colours from the scene palette: ink = the tight outline shadow, shadow = the wide soft glow, glow = line 2.
A dark custom palette suits it, e.g. { bg: [...], ink: 'rgba(4,18,38,.92)', shadow: 'rgba(4,22,48,.55)', glow: '#ffd76a' }. -->
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: var(--W); height: var(--H); overflow: hidden; background: #000; }
#c { position: relative; width: var(--W); height: var(--H); overflow: hidden; }
#c > img.frame { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.cap { position: absolute; max-width: calc(var(--W) * var(--mw)); display: flex; flex-direction: column; gap: calc(var(--fs) * .06);
font-family: var(--tf); font-weight: var(--tw); font-size: var(--fs); line-height: var(--lh); color: #fff; letter-spacing: var(--ls, .005em);
text-wrap: balance;
/* a dark outline-shadow for edges, then a wide soft glow: readable on light water and on dark rock alike */
text-shadow: 0 0 calc(var(--fs) * .05) var(--ink), 0 calc(var(--fs) * .04) calc(var(--fs) * .08) var(--ink),
0 0 calc(var(--fs) * .3) var(--shadow), 0 0 calc(var(--fs) * .6) var(--shadow); }
.cap span { display: block; }
.cap span + span { color: var(--accent); }
.cap.tl { left: var(--mx); top: var(--my); }
.cap.tr { right: var(--mx); top: var(--my); text-align: right; align-items: flex-end; }
.cap.bl { left: var(--mx); bottom: var(--my); }
.cap.br { right: var(--mx); bottom: var(--my); text-align: right; align-items: flex-end; }
.cap[dir="rtl"].tl, .cap[dir="rtl"].bl { text-align: right; }
</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'), SID = q.get('s'), V = q.get('v') ?? '', SIZE = q.get('size') ?? '';
const job = await (await fetch(`/job.json?lang=${encodeURIComponent(LANG)}&s=${encodeURIComponent(SID)}&v=${encodeURIComponent(V)}`)).json();
const S = job.scene, C = job.copy, P = job.palette;
const R = document.documentElement.style;
document.documentElement.lang = LANG;
const short = Math.min(W, H);
R.setProperty('--W', W + 'px'); R.setProperty('--H', H + 'px');
// caption size from the short side: about 9 % of it on a landscape phone, a little less on squarer canvases
const fs = short * (W / H > 1.9 ? .092 : W / H > 1.5 ? .08 : .066) * (S.captionScale ?? 1);
R.setProperty('--fs', fs + 'px');
const inset = S.captionInset ?? .05; // distance from the corner, share of the short side
R.setProperty('--mx', (short * (inset + .005)) + 'px'); R.setProperty('--my', (short * inset) + 'px');
// the caption keeps to its corner: at most 40 % of a landscape image's width (scene.captionWidth), so it stays clear of
// what an app centres at the top
const MW = S.captionWidth ?? (W > H ? .4 : .84);
R.setProperty('--mw', MW);
// palette: ink = the tight outline shadow, shadow = the wide soft glow, glow = the second line's colour
R.setProperty('--ink', P.ink); R.setProperty('--shadow', P.shadow); R.setProperty('--accent', P.glow);
const X = job.fonts.script;
const fstack = (st, def) => (st ?? (X ? [X, def] : [def])).map(f => `'${f}'`).join(', ') + ', sans-serif';
R.setProperty('--tf', fstack(job.fonts.titleStack, job.fonts.title));
R.setProperty('--tw', X ? 900 : job.fonts.titleWeight ?? 400);
const CJK = /^(ja|ko|zh)/.test(LANG);
R.setProperty('--lh', /^(ar|hi|fa|ur|th)/.test(LANG) ? 1.3 : CJK ? 1.16 : 1.04);
if (X) R.setProperty('--ls', '0');
if (LANG.startsWith('ja')) document.documentElement.style.wordBreak = 'auto-phrase';
if (LANG.startsWith('ko')) document.documentElement.style.wordBreak = 'keep-all';
const RTL = !!job.rtl;
const esc = s => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;');
const corner = ['tl', 'tr', 'bl', 'br'].includes(S.corner) ? S.corner : 'tl';
const src = `/frames/${encodeURIComponent(LANG)}/${encodeURIComponent(SIZE)}/${encodeURIComponent(S.frames[0].src)}`;
const lines = (C.title ?? []).filter(Boolean);
const c = document.getElementById('c');
c.innerHTML = `<img class="frame" src="${src}"><div class="cap ${corner}"${RTL ? ' dir="rtl"' : ''}>${lines.map(l => `<span>${esc(l)}</span>`).join('')}</div>`;
await document.fonts.ready;
await Promise.all([...document.images].map(i => i.decode().catch(() => {})));
const overflow = []; // read by `shots render` -> report.json -> shots check / gallery
const img = c.querySelector('img.frame');
if (!img.naturalWidth) overflow.push(`frame ${SIZE}/${S.frames[0].src} did not load`);
else {
const ar = img.naturalWidth / img.naturalHeight, want = W / H;
if (Math.abs(ar / want - 1) > .01) overflow.push(`frame ${img.naturalWidth}x${img.naturalHeight} is cropped to fit ${W}x${H}: capture it at the size's aspect`);
if (img.naturalWidth < W * .99) overflow.push(`frame ${img.naturalWidth}x${img.naturalHeight} is upscaled to ${W}x${H}: capture it at native resolution`);
}
const cap = c.querySelector('.cap');
const maxW = W * MW;
for (const [i, sp] of [...cap.querySelectorAll('span')].entries()) { // shrink a line that would not fit its corner
const wd = sp.scrollWidth;
let f = 1; while (wd * f > maxW && f > .6) f -= .02;
if (f < 1) sp.style.fontSize = `${(fs * f).toFixed(1)}px`;
if (wd * f > maxW + 1) overflow.push(`caption line ${i + 1} is too long even at 60 % size: shorten it`);
}
const r = cap.getBoundingClientRect();
if (r.left < 0 || r.top < 0 || r.right > W || r.bottom > H) overflow.push('caption runs off the image');
if (r.height > H * .42) overflow.push(`caption covers ${Math.round(r.height / H * 100)} % of the height: shorten it`);
if (lines.join(' ').split(/\s+/).length > 6 && !CJK && !/^th/.test(LANG)) overflow.push('caption has more than 6 words: keep it to 3-5');
document.body.dataset.overflow = JSON.stringify(overflow);
document.body.dataset.devices = '[]';
document.body.dataset.ready = '1';
</script>
</body>
</html>