bf5b07962d
scripts/promo.js builds an animated portrait promo from the store screenshots (branded slides + Persian captions + Ken Burns), records it with Playwright, and is encoded to store-assets/promo.mp4 via the system ffmpeg. Output dir gitignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
5.0 KiB
JavaScript
101 lines
5.0 KiB
JavaScript
// Build an animated portrait promo from the captured screenshots and record it
|
|
// to webm with Playwright. (ffmpeg then encodes it to mp4 — see the run step.)
|
|
const { chromium } = require("playwright");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const ASSETS = path.join(__dirname, "..", "store-assets");
|
|
const OUT = path.join(__dirname, "promo");
|
|
fs.mkdirSync(OUT, { recursive: true });
|
|
|
|
const b64 = (f) => "data:image/png;base64," + fs.readFileSync(path.join(ASSETS, f)).toString("base64");
|
|
const icon = b64("icon-512.png");
|
|
|
|
// type: intro | slide | outro
|
|
const SLIDES = [
|
|
{ type: "intro", title: "برگ وسط", sub: "بازی حکم آنلاین ایرانی" },
|
|
{ type: "slide", img: b64("01-home.png"), cap: "سه حالت بازی در یک اپ" },
|
|
{ type: "slide", img: b64("06-game.png"), cap: "حکمِ واقعی، کارتهای زیبا" },
|
|
{ type: "slide", img: b64("02-leaderboard.png"), cap: "در لیگها بالا برو و رکورد بزن" },
|
|
{ type: "slide", img: b64("04-shop.png"), cap: "آواتار و آیتمهای ویژه" },
|
|
{ type: "slide", img: b64("03-achievements.png"), cap: "دستاوردها و جایزهی روزانه" },
|
|
{ type: "slide", img: b64("05-profile.png"), cap: "پروفایل کامل خودت را بساز" },
|
|
{ type: "outro", title: "همین حالا رایگان نصب کن!", sub: "برگ وسط", icon: true },
|
|
];
|
|
const DUR = 2600; // ms per slide
|
|
const FADE = 700;
|
|
|
|
const slideHtml = (s, i) => {
|
|
if (s.type === "intro" || s.type === "outro")
|
|
return `<div class="slide center" data-i="${i}">
|
|
<img class="icon" src="${icon}"/>
|
|
<div class="title">${s.title}</div>
|
|
<div class="sub">${s.sub}</div>
|
|
</div>`;
|
|
return `<div class="slide" data-i="${i}">
|
|
<div class="brand">برگ وسط</div>
|
|
<div class="phonewrap"><img class="phone" src="${s.img}"/></div>
|
|
<div class="cap">${s.cap}</div>
|
|
</div>`;
|
|
};
|
|
|
|
const html = `<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8">
|
|
<style>
|
|
* { margin:0; padding:0; box-sizing:border-box; font-family: Tahoma, "Segoe UI", sans-serif; }
|
|
html,body { width:1080px; height:1920px; overflow:hidden; }
|
|
.stage { position:relative; width:1080px; height:1920px;
|
|
background: radial-gradient(120% 80% at 50% 0%, #123 0%, #0e1c3f 38%, #060c1f 100%); }
|
|
.stage::after { content:""; position:absolute; inset:0;
|
|
background: radial-gradient(60% 35% at 50% 42%, rgba(212,175,55,.16), transparent 70%); }
|
|
.slide { position:absolute; inset:0; opacity:0; transition: opacity ${FADE}ms ease;
|
|
display:flex; flex-direction:column; align-items:center; padding:70px 60px; }
|
|
.slide.active { opacity:1; }
|
|
.brand { color:#d4af37; font-size:46px; font-weight:800; letter-spacing:1px;
|
|
opacity:.85; margin-bottom:18px; }
|
|
.phonewrap { flex:1; display:flex; align-items:center; justify-content:center; min-height:0; }
|
|
.phone { max-height:1300px; max-width:660px; border-radius:42px;
|
|
box-shadow: 0 30px 80px rgba(0,0,0,.55), 0 0 0 2px rgba(212,175,55,.25);
|
|
transform: scale(1.0); transition: transform ${DUR + FADE}ms ease-out; }
|
|
.slide.active .phone { transform: scale(1.07); }
|
|
.cap { color:#fdf6e3; font-size:58px; font-weight:800; text-align:center; line-height:1.5;
|
|
text-shadow:0 3px 18px rgba(0,0,0,.6); margin-top:34px;
|
|
transform: translateY(24px); opacity:0; transition: all ${FADE}ms ease ${FADE / 2}ms; }
|
|
.slide.active .cap { transform: translateY(0); opacity:1; }
|
|
.center { justify-content:center; gap:40px; }
|
|
.center .icon { width:360px; height:360px; border-radius:80px;
|
|
box-shadow:0 24px 70px rgba(0,0,0,.5); transform:scale(.8); transition:transform 900ms ease; }
|
|
.center.active .icon { transform:scale(1); }
|
|
.center .title { color:#f1da8a; font-size:96px; font-weight:800; text-align:center;
|
|
text-shadow:0 4px 24px rgba(0,0,0,.5); }
|
|
.center .sub { color:#cbd5e1; font-size:50px; font-weight:600; text-align:center; }
|
|
/* intro/outro icon hidden when not requested */
|
|
</style></head><body>
|
|
<div class="stage">${SLIDES.map(slideHtml).join("")}</div>
|
|
<script>
|
|
const slides=[...document.querySelectorAll('.slide')];
|
|
const DUR=${DUR};
|
|
let i=0;
|
|
function show(n){ slides.forEach((s,k)=>s.classList.toggle('active',k===n)); }
|
|
show(0);
|
|
const timer=setInterval(()=>{ i++; if(i>=slides.length){ clearInterval(timer); window.__done=true; return;} show(i); }, DUR);
|
|
</script>
|
|
</body></html>`;
|
|
|
|
(async () => {
|
|
const total = SLIDES.length * DUR + 1200;
|
|
const browser = await chromium.launch({ channel: "chrome" });
|
|
const ctx = await browser.newContext({
|
|
viewport: { width: 1080, height: 1920 },
|
|
deviceScaleFactor: 1,
|
|
recordVideo: { dir: OUT, size: { width: 1080, height: 1920 } },
|
|
});
|
|
const page = await ctx.newPage();
|
|
await page.setContent(html, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(total);
|
|
const video = page.video();
|
|
await ctx.close(); // finalizes the recording
|
|
const p = await video.path();
|
|
await browser.close();
|
|
console.log("VIDEO:" + p);
|
|
})().catch((e) => { console.error("FATAL", e); process.exit(1); });
|