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
|
|
@ -10,6 +10,9 @@
|
|||
sunburst, no sparkles; badges only when the copy has them, drawn as quiet chips. Same /job.json as template.html
|
||||
(palette: bg = gradient stops, ink = headline, glow = accent, shadow = device shadow).
|
||||
Layout: everything is in u = W / 100; the text block is measured first, then the device fills the room below it.
|
||||
device=landscape: u = H / 100, copy column on one side, the landscape frame in a Mac window (frame=window) or bare
|
||||
(frame=none) on the other (as template.html). Safe margins: 7 % of the width left / right, 5 % of the height at the
|
||||
top; text and logo boxes go to body.dataset.boxes for shots check.
|
||||
-->
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
|
@ -20,7 +23,7 @@
|
|||
transform: translate(-50%, -50%); border-radius: 50%; background: radial-gradient(closest-side, var(--accentSoft), transparent); }
|
||||
|
||||
.top { position: absolute; left: 0; right: 0; top: calc(var(--u) * var(--top)); text-align: center; z-index: 6;
|
||||
padding: 0 calc(var(--u) * 7); display: flex; flex-direction: column; align-items: center; }
|
||||
padding: 0 calc(var(--W) * .07); display: flex; flex-direction: column; align-items: center; }
|
||||
.eyebrow { margin-bottom: calc(var(--u) * 2.4); font-size: calc(var(--u) * 2.9); font-weight: 700; letter-spacing: var(--kls, .14em);
|
||||
text-transform: uppercase; color: var(--accent); line-height: 1.2; }
|
||||
h1 { font-family: var(--tf); font-weight: var(--tw); line-height: var(--lh); letter-spacing: var(--ls, -.02em);
|
||||
|
|
@ -29,7 +32,7 @@
|
|||
h1 span + span { color: var(--line2); }
|
||||
p.sub { margin-top: calc(var(--u) * 2.4); max-width: calc(var(--u) * 78); font-size: calc(var(--u) * var(--sub));
|
||||
line-height: 1.38; font-weight: 500; color: var(--ink); opacity: .72; text-wrap: balance; }
|
||||
.logo { width: calc(var(--u) * 60); max-height: calc(var(--u) * 26); object-fit: contain; margin-bottom: calc(var(--u) * 3); }
|
||||
.logo { width: min(100%, calc(var(--u) * 60)); max-height: calc(var(--u) * 26); object-fit: contain; margin-bottom: calc(var(--u) * 3); }
|
||||
|
||||
/* device: a plain modern phone, dark bezel, soft shadow */
|
||||
.tel { position: absolute; z-index: 3; width: var(--pw); aspect-ratio: var(--ar); left: var(--px); top: var(--py);
|
||||
|
|
@ -57,6 +60,14 @@
|
|||
.android .island { width: calc(var(--pw) * .013); height: calc(var(--pw) * .013); top: calc(var(--pw) * .009); }
|
||||
.android .bar { padding: 0 4%; }
|
||||
|
||||
.land .top { right: auto; width: var(--colW); padding: 0; left: var(--colL); }
|
||||
.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 calc(var(--u) * 2) calc(var(--u) * 4) rgba(0,0,0,.08), 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: #e8e8ea; }
|
||||
.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; }
|
||||
.chip { position: absolute; z-index: 9; display: flex; flex-direction: column; padding: calc(var(--u) * 1.8) calc(var(--u) * 3);
|
||||
border-radius: calc(var(--u) * 3); background: rgba(255,255,255,.92); color: #111827; white-space: nowrap;
|
||||
box-shadow: 0 calc(var(--u) * 1) calc(var(--u) * 3.5) rgba(15,23,42,.14); }
|
||||
|
|
@ -69,12 +80,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;
|
||||
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;
|
||||
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;
|
||||
|
|
@ -101,10 +114,11 @@ 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, wide = Hu < 150;
|
||||
R.setProperty('--top', tall ? 9.5 : wide ? 4.5 : 6);
|
||||
R.setProperty('--fs', tall ? 8.6 : wide ? 5.6 : 7);
|
||||
R.setProperty('--sub', tall ? 3.7 : wide ? 2.6 : 3.2);
|
||||
if (LAND) { document.body.classList.add('land'); R.setProperty('--colW', colW + 'px'); R.setProperty('--colL', colL + 'px'); }
|
||||
const tall = !LAND && Hu > 200, wide = !LAND && Hu < 150;
|
||||
R.setProperty('--top', Math.max(tall ? 9.5 : wide ? 4.5 : 6, MT / u));
|
||||
R.setProperty('--fs', LAND ? 8.5 : tall ? 8.6 : wide ? 5.6 : 7);
|
||||
R.setProperty('--sub', LAND ? 3.4 : tall ? 3.7 : wide ? 2.6 : 3.2);
|
||||
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)}%`);
|
||||
|
|
@ -124,7 +138,7 @@ await document.fonts.ready;
|
|||
await Promise.all([...document.images].map(i => i.decode().catch(() => {})));
|
||||
const overflow = [];
|
||||
for (const [i, sp] of [...c.querySelectorAll('h1 span')].entries()) {
|
||||
const max = W - 12 * 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`);
|
||||
|
|
@ -133,6 +147,21 @@ for (const [i, sp] of [...c.querySelectorAll('h1 span')].entries()) {
|
|||
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 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('--softY', H / 2 + 'px');
|
||||
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];
|
||||
|
|
@ -177,9 +206,12 @@ c.insertAdjacentHTML('beforeend', html);
|
|||
await Promise.all([...document.images].map(i => i.decode().catch(() => {})));
|
||||
if (textBottom > Hu * .45) overflow.push(`text block covers ${Math.round(textBottom / Hu * 100)} % of the height (the app should be the star)`);
|
||||
for (const b of c.querySelectorAll('.chip')) { 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);
|
||||
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