72 lines
6 KiB
HTML
72 lines
6 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<link rel="stylesheet" href="/fonts/fonts.css">
|
|
<!-- Play feature graphic, 1024 x 500: logo (or the app name) on the left, a tilted phone with one frame on the right.
|
|
feature.html?lang=<id>[&v=<variant>]. Rendered by `storebound shots render`. Keep text out of it: Play overlays its own UI. -->
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
html, body { width: 1024px; height: 500px; overflow: hidden; background: #000; }
|
|
#c { position: relative; width: 1024px; height: 500px; overflow: hidden; isolation: isolate; background: var(--bg); }
|
|
.rays { position: absolute; left: 360px; top: 250px; width: 1700px; height: 1700px; transform: translate(-50%, -50%) rotate(8deg); z-index: -4;
|
|
opacity: .55; background: repeating-conic-gradient(rgba(255,255,255,.22) 0deg 7deg, rgba(255,255,255,0) 7deg 18deg);
|
|
-webkit-mask-image: radial-gradient(closest-side, #000 0%, rgba(0,0,0,.6) 30%, transparent 75%); }
|
|
.glow { position: absolute; left: 360px; top: 250px; width: 900px; height: 700px; transform: translate(-50%, -50%); z-index: -3;
|
|
background: radial-gradient(closest-side, var(--glow), transparent); }
|
|
.vign { position: absolute; inset: 0; z-index: -2; background: radial-gradient(90% 90% at 45% 50%, transparent 60%, rgba(0,0,0,.28) 100%); }
|
|
.logo { position: absolute; left: 360px; top: 252px; width: 540px; max-height: 400px; object-fit: contain; transform: translate(-50%, -50%) rotate(-3deg); z-index: 5;
|
|
filter: drop-shadow(0 14px 22px rgba(0,0,0,.4)); }
|
|
.name { position: absolute; left: 360px; top: 250px; transform: translate(-50%, -50%) rotate(-3deg); z-index: 5; width: 540px; text-align: center;
|
|
font-family: var(--tf); font-size: 104px; line-height: .95; color: #fff; -webkit-text-stroke: 9px var(--ink); paint-order: stroke fill;
|
|
filter: drop-shadow(0 8px 0 var(--ink)) drop-shadow(0 14px 22px rgba(0,0,0,.35)); }
|
|
.tel { position: absolute; left: 748px; top: 36px; width: 232px; aspect-ratio: var(--ar); transform: rotate(7deg); z-index: 3;
|
|
border-radius: 40px; padding: 6px;
|
|
background: linear-gradient(140deg, #f4f4f6 0%, #9da0a8 8%, #3c3e44 20%, #1a1b1f 50%, #3c3e44 82%, #c8cad0 100%);
|
|
box-shadow: inset 0 0 0 1px rgba(255,255,255,.6), inset 0 0 0 3px #101114, 0 20px 50px var(--shadow); }
|
|
.screen { position: relative; width: 100%; height: 100%; overflow: hidden; border-radius: 34px; background: #000; }
|
|
.screen img { position: absolute; left: 0; bottom: 0; width: 100%; height: var(--appH); object-fit: cover; object-position: top; }
|
|
.screen .bar { position: absolute; left: 0; right: 0; top: 0; height: var(--barH); background: #0b0b10 center top / 100% 60000% no-repeat; }
|
|
.island { position: absolute; top: 6px; left: 50%; transform: translateX(-50%); width: 30%; height: 20px; background: #000; border-radius: 99px; }
|
|
.spark { position: absolute; width: var(--s); height: var(--s); transform: translate(-50%, -50%); background: #fff; z-index: -1;
|
|
clip-path: polygon(50% 0, 58% 42%, 100% 50%, 58% 58%, 50% 100%, 42% 58%, 0 50%, 42% 42%); opacity: var(--o); filter: drop-shadow(0 0 6px #fff); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="c">
|
|
<div class="glow"></div><div class="rays"></div><div class="vign"></div>
|
|
<div class="tel"><div class="screen"><div class="bar" id="bar"></div><img id="f"><div class="island"></div></div></div>
|
|
</div>
|
|
<script type="module">
|
|
const q = new URLSearchParams(location.search);
|
|
const lang = q.get('lang'), V = q.get('v') ?? '';
|
|
const job = await (await fetch(`/job.json?lang=${encodeURIComponent(lang)}&feature=1&v=${encodeURIComponent(V)}`)).json();
|
|
const P = job.palette, R = document.documentElement.style, dev = job.phone;
|
|
R.setProperty('--bg', P.bgCss ?? (Array.isArray(P.bg) ? `linear-gradient(160deg, ${P.bg.join(', ')})` : P.bg)); // palette.ts: stops, colour, gradient string or photo + overlay
|
|
R.setProperty('--glow', P.glow); R.setProperty('--ink', P.ink); R.setProperty('--shadow', P.shadow);
|
|
R.setProperty('--tf', (job.fonts.titleStack ?? [job.fonts.title]).map(f => `'${f}'`).join(', ') + ', sans-serif');
|
|
R.setProperty('--ar', `${dev.w} / ${dev.h + dev.bar}`);
|
|
R.setProperty('--appH', `${100 * dev.h / (dev.h + dev.bar)}%`);
|
|
R.setProperty('--barH', `${100 * dev.bar / (dev.h + dev.bar)}%`);
|
|
const c = document.getElementById('c');
|
|
// device place and size from featureLayout() (lib.ts): fit 'contain' keeps the rotated phone inside the canvas
|
|
const D = job.device, tel = c.querySelector('.tel');
|
|
if (D) Object.assign(tel.style, { left: D.left + 'px', top: D.top + 'px', width: D.width + 'px', transform: `rotate(${D.angle}deg)` });
|
|
const src = `/frames/${encodeURIComponent(lang)}/phone/${encodeURIComponent(job.frame)}`;
|
|
document.getElementById('f').src = src;
|
|
document.getElementById('bar').style.backgroundImage = `url('${src}')`;
|
|
c.insertAdjacentHTML('beforeend', job.logo ? '<img class="logo" src="/logo">' : `<div class="name">${String(job.appName).replace(/</g, '<')}</div>`);
|
|
let seed = 7; const rnd = () => ((seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296);
|
|
for (let i = 0; i < 22; i++)
|
|
c.insertAdjacentHTML('beforeend', `<i class="spark" style="left:${rnd() * 1024}px;top:${rnd() * 500}px;--s:${8 + rnd() * 18}px;--o:${.5 + rnd() * .5}"></i>`);
|
|
await document.fonts.ready;
|
|
await Promise.all([...document.images].map(i => i.decode().catch(() => {})));
|
|
// read by `shots render` -> report.json -> shots check: the phone's rotated outline must stay inside the canvas
|
|
document.body.dataset.devices = JSON.stringify([...c.querySelectorAll('.tel')].map(t => { const r = t.getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height }; }));
|
|
if (D?.fit === 'bleed') document.body.dataset.bleed = '1';
|
|
// logo / name box for the safe-margin rule (7 % of the width left / right, 5 % of the height at the top)
|
|
document.body.dataset.boxes = JSON.stringify([...c.querySelectorAll('.logo, .name')].map(e => { const r = e.getBoundingClientRect(); return { el: e.matches('.logo') ? 'logo' : 'title', x: r.x, y: r.y, w: r.width, h: r.height }; }));
|
|
document.body.dataset.ready = '1';
|
|
</script>
|
|
</body>
|
|
</html>
|