97 lines
6.4 KiB
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, '&').replace(/</g, '<');
|
|
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>
|