shots: Mac App Store screenshots (mac size, APP_DESKTOP to the macOS version), a landscape layout (Mac window or bare; Mac, Play Chromebook, Play tablets), safe margins in every template checked by shots check from the render's layout boxes, a contact sheet timeout that scales with images, sizes and load; pitfall 80
This commit is contained in:
parent
30488c512d
commit
e59539fec8
26 changed files with 532 additions and 67 deletions
|
|
@ -9,6 +9,11 @@
|
|||
style=android (Play 7" / 10" tablets): a plain Android tablet body (thin dark bezel, punch-hole camera) instead of the iPad look.
|
||||
/job.json gives the scene (palette, frames, badge slots), the words for this language and the font families.
|
||||
Layout: everything is in u = W / 100; the text block is measured first, then the phone fills the room below it.
|
||||
device=landscape (Mac, Chromebook, Play tablets with shots.landscape.playTablets): u = H / 100, the copy in a column on
|
||||
one side (job.side), the first frame of the scene from frames/<lang>/landscape/ on the other, in a Mac window
|
||||
(frame=window) or bare with rounded corners (frame=none). Badges are not drawn in landscape.
|
||||
Safe margins (lib.ts SAFE_MARGIN): text and logo keep 7 % of the width left and right and 5 % of the height at the
|
||||
top; their boxes go to body.dataset.boxes -> report.json -> shots check.
|
||||
-->
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
|
@ -34,7 +39,7 @@
|
|||
|
||||
/* text block */
|
||||
.top { position: absolute; left: 0; right: 0; top: calc(var(--u) * var(--top)); text-align: center; z-index: 6;
|
||||
padding: 0 calc(var(--u) * 5); display: flex; flex-direction: column; align-items: center; }
|
||||
padding: 0 calc(var(--W) * .07); display: flex; flex-direction: column; align-items: center; }
|
||||
.eyebrow { display: inline-flex; align-items: center; gap: calc(var(--u) * 1.6); margin-bottom: calc(var(--u) * 3);
|
||||
padding: calc(var(--u) * 1.15) calc(var(--u) * 3) calc(var(--u) * 1.15) calc(var(--u) * 1.3); border-radius: 999px;
|
||||
background: rgba(0,0,0,.26); box-shadow: inset 0 0 0 calc(var(--u) * .25) rgba(255,255,255,.28);
|
||||
|
|
@ -51,7 +56,7 @@
|
|||
p.sub { margin-top: calc(var(--u) * 2.6); max-width: calc(var(--u) * 80); font-size: calc(var(--u) * var(--sub));
|
||||
line-height: 1.32; font-weight: 700; color: #fff; text-wrap: balance;
|
||||
text-shadow: 0 calc(var(--u) * .3) calc(var(--u) * 1) rgba(0,0,0,.35); }
|
||||
.logo { width: calc(var(--u) * 86); max-height: calc(var(--u) * 36); object-fit: contain; margin: calc(var(--u) * -1) 0 calc(var(--u) * 1);
|
||||
.logo { width: 100%; max-height: calc(var(--u) * 32); object-fit: contain; margin: 0 0 calc(var(--u) * 1);
|
||||
filter: drop-shadow(0 calc(var(--u) * 1.5) calc(var(--u) * 2.5) rgba(0,0,0,.35)); }
|
||||
|
||||
/* phone */
|
||||
|
|
@ -84,6 +89,20 @@
|
|||
.android .island { width: calc(var(--pw) * .013); height: calc(var(--pw) * .013); top: calc(var(--pw) * .009); }
|
||||
.android .bar { padding: 0 4%; }
|
||||
|
||||
/* landscape: copy column + a Mac window (or the bare frame) */
|
||||
.land .top { right: auto; width: var(--colW); padding: 0; left: var(--colL); }
|
||||
.land h1 span { margin: 0 auto; }
|
||||
.lwin { position: absolute; z-index: 3; left: var(--lx); top: var(--ly); width: var(--lw); border-radius: calc(var(--lw) * .012);
|
||||
overflow: hidden; background: #1c1c1e; box-shadow: 0 0 0 calc(var(--u) * .12) rgba(255,255,255,.18),
|
||||
0 calc(var(--u) * 2) calc(var(--u) * 5) rgba(0,0,0,.35), 0 calc(var(--u) * 5) calc(var(--u) * 12) var(--shadow); }
|
||||
.lwin .tb { height: calc(var(--lw) * .028); display: flex; align-items: center; gap: calc(var(--lw) * .006); padding-left: calc(var(--lw) * .012);
|
||||
background: linear-gradient(180deg, #3a3a3c, #2c2c2e); }
|
||||
.lwin .tb i { width: calc(var(--lw) * .009); height: calc(var(--lw) * .009); border-radius: 50%; background: #ff5f57; }
|
||||
.lwin .tb i:nth-child(2) { background: #febc2e; } .lwin .tb i:nth-child(3) { background: #28c840; }
|
||||
.lwin img { display: block; width: 100%; aspect-ratio: var(--lar); object-fit: cover; }
|
||||
.lwin.bare { border-radius: calc(var(--lw) * .02); }
|
||||
.lwin.bare .tb { display: none; }
|
||||
|
||||
/* badges */
|
||||
.badge { position: absolute; z-index: 9; display: flex; align-items: center; gap: calc(var(--u) * 2.2);
|
||||
padding: calc(var(--u) * 1.7) calc(var(--u) * 3.6) calc(var(--u) * 1.7) calc(var(--u) * 1.7); border-radius: calc(var(--u) * 4);
|
||||
|
|
@ -110,12 +129,14 @@
|
|||
<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') ?? '';
|
||||
const tablet = q.get('device') === 'tablet';
|
||||
const tablet = q.get('device') === 'tablet', LAND = q.get('device') === 'landscape';
|
||||
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 u = W / 100, Hu = H / u;
|
||||
const dev = tablet ? job.tablet : job.phone; // { w, h, bar } in CSS px of the raw frame
|
||||
const FR = `/frames/${encodeURIComponent(LANG)}/${tablet ? 'tablet' : 'phone'}/`;
|
||||
const u = LAND ? H / 100 : W / 100, Hu = H / u;
|
||||
const dev = LAND ? job.landscape : tablet ? job.tablet : job.phone; // { w, h, bar } in CSS px of the raw frame
|
||||
const FR = `/frames/${encodeURIComponent(LANG)}/${LAND ? 'landscape' : tablet ? 'tablet' : 'phone'}/`;
|
||||
const MX = W * .07, MT = H * .05; // safe margins (lib.ts SAFE_MARGIN)
|
||||
const colW = W * .36, colL = job.side === 'right' ? W - MX - colW : MX;
|
||||
|
||||
document.documentElement.lang = LANG;
|
||||
const RTL = !!job.rtl;
|
||||
|
|
@ -139,11 +160,12 @@ if (LANG.startsWith('ja')) document.documentElement.style.wordBreak = 'auto-phra
|
|||
if (LANG.startsWith('ko')) document.documentElement.style.wordBreak = 'keep-all';
|
||||
if (tablet) document.body.classList.add('tablet');
|
||||
if (q.get('style') === 'android') document.body.classList.add('android');
|
||||
const tall = Hu > 200; // iOS 6.9" / 6.5" vs 16:9
|
||||
if (LAND) { document.body.classList.add('land'); R.setProperty('--colW', colW + 'px'); R.setProperty('--colL', colL + 'px'); }
|
||||
const tall = !LAND && Hu > 200; // iOS 6.9" / 6.5" vs 16:9
|
||||
const wide = Hu < 150; // 3:4 iPad
|
||||
R.setProperty('--top', tall ? 9 : wide ? 4 : 5.5);
|
||||
R.setProperty('--fs', S.hero ? (tall ? 10.5 : wide ? 7 : 8.6) : (tall ? 12 : wide ? 7.6 : 9.8));
|
||||
R.setProperty('--sub', tall ? 3.75 : wide ? 2.7 : 3.3);
|
||||
R.setProperty('--top', Math.max(tall ? 9 : wide ? 4 : 5.5, MT / u));
|
||||
R.setProperty('--fs', LAND ? (S.hero ? 9 : 10) : S.hero ? (tall ? 10.5 : wide ? 7 : 8.6) : (tall ? 12 : wide ? 7.6 : 9.8));
|
||||
R.setProperty('--sub', LAND ? 3.6 : tall ? 3.75 : wide ? 2.7 : 3.3);
|
||||
const AR = dev.w / (dev.h + dev.bar);
|
||||
R.setProperty('--ar', `${dev.w} / ${dev.h + dev.bar}`);
|
||||
R.setProperty('--appH', `${100 * dev.h / (dev.h + dev.bar)}%`);
|
||||
|
|
@ -188,7 +210,7 @@ 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
|
||||
for (const [i, sp] of [...c.querySelectorAll('h1 span')].entries()) { // shrink a line that would overflow (long words)
|
||||
const max = W - 10 * u, wd = sp.getBoundingClientRect().width;
|
||||
const max = LAND ? colW : W - 2 * MX, wd = sp.getBoundingClientRect().width;
|
||||
let f = 1; while (wd * f > max && f > .55) f -= .02;
|
||||
if (f < 1) sp.style.fontSize = `calc(${getComputedStyle(sp).fontSize} * ${f.toFixed(2)})`;
|
||||
if (wd * f > max + 1) overflow.push(`title line ${i + 1} is too long even at 55 % size: shorten it`);
|
||||
|
|
@ -197,8 +219,24 @@ for (const [i, sp] of [...c.querySelectorAll('h1 span')].entries()) { // shrink
|
|||
const subEl = c.querySelector('p.sub');
|
||||
if (subEl) { const lines = Math.round(subEl.getBoundingClientRect().height / parseFloat(getComputedStyle(subEl).lineHeight)); if (lines > 3) overflow.push(`subtitle wraps to ${lines} lines (max 3)`); }
|
||||
const eb = c.querySelector('.eyebrow');
|
||||
if (eb && eb.getBoundingClientRect().width > W - 8 * u) overflow.push('eyebrow is wider than the image');
|
||||
if (eb && eb.getBoundingClientRect().width > (LAND ? colW : W - 2 * MX)) overflow.push('eyebrow is wider than the image');
|
||||
const textBottom = c.querySelector('.top').getBoundingClientRect().bottom / u;
|
||||
portrait: {
|
||||
if (LAND) {
|
||||
const topEl = c.querySelector('.top'), bh = topEl.getBoundingClientRect().height;
|
||||
topEl.style.top = Math.max(MT, (H - bh) / 2) + 'px';
|
||||
if (bh > H - MT * 2) overflow.push('text block is taller than the image: shorten the copy');
|
||||
const f = S.frames[0], src = FR + encodeURIComponent(f.src), bare = q.get('frame') === 'none';
|
||||
const x0 = job.side === 'right' ? W * .04 : colL + colW + W * .05, x1 = job.side === 'right' ? colL - W * .05 : W - W * .04;
|
||||
let lw = x1 - x0;
|
||||
const lh = k => k / (dev.w / dev.h) + (bare ? 0 : k * .028);
|
||||
if (lh(lw) > H * .88) lw = lw * H * .88 / lh(lw);
|
||||
R.setProperty('--lar', `${dev.w} / ${dev.h}`);
|
||||
R.setProperty('--rayY', H / 2 + 'px'); R.setProperty('--rayRot', (rnd() * 20) + 'deg');
|
||||
c.insertAdjacentHTML('beforeend', `<div class="lwin front${bare ? ' bare' : ''}" style="--lw:${lw}px;--lx:${(x0 + x1 - lw) / 2}px;--ly:${(H - lh(lw)) / 2}px"><div class="tb"><i></i><i></i><i></i></div><img src="${src}"></div>`);
|
||||
await Promise.all([...document.images].map(i => i.decode().catch(() => {})));
|
||||
break portrait;
|
||||
}
|
||||
|
||||
const frames = S.frames.map(f => ({ x: 0, dy: 0, scale: 1, rotate: 0, ...f }));
|
||||
const front = frames[0];
|
||||
|
|
@ -244,9 +282,13 @@ c.insertAdjacentHTML('beforeend', html);
|
|||
await Promise.all([...document.images].map(i => i.decode().catch(() => {})));
|
||||
if (textBottom > Hu * .48) overflow.push(`text block covers ${Math.round(textBottom / Hu * 100)} % of the height (the game should be the star)`);
|
||||
for (const b of c.querySelectorAll('.badge')) { const r = b.getBoundingClientRect(); if (r.left < 0 || r.right > W || r.bottom > H) overflow.push(`badge "${b.textContent.trim().slice(0, 30)}" runs off the image`); }
|
||||
}
|
||||
document.body.dataset.overflow = JSON.stringify(overflow);
|
||||
// text and logo boxes for the safe-margin rule (shots check)
|
||||
document.body.dataset.boxes = JSON.stringify([...c.querySelectorAll('.top .logo, .top .eyebrow, .top h1 span, .top p.sub')].map(e => { const r = e.getBoundingClientRect();
|
||||
return { el: e.matches('.logo') ? 'logo' : e.matches('.eyebrow') ? 'eyebrow' : e.matches('p') ? 'subtitle' : 'title', x: r.x, y: r.y, w: r.width, h: r.height }; }).filter(b => b.w > 0 && b.h > 0));
|
||||
// the front device only: back phones of a fanned layout peek in from the side on purpose
|
||||
document.body.dataset.devices = JSON.stringify([...c.querySelectorAll('.tel.front')].map(t => { const r = t.getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height }; }));
|
||||
document.body.dataset.devices = JSON.stringify([...c.querySelectorAll('.tel.front, .lwin')].map(t => { const r = t.getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height }; }));
|
||||
document.body.dataset.ready = '1';
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue