← back to Restaurant Directory
videos/render-cards.mjs
97 lines
// Render all 1280x720 cards (title, sxs-light, sxs-dark, findings, outro) as PNGs via Playwright.
// Replaces ffmpeg drawtext (not in our ffmpeg 8.1 build) for ALL text composition.
import { chromium } from 'playwright';
import path from 'node:path';
import fs from 'node:fs/promises';
const OUT = process.argv[2];
const ITER = process.argv[3]; // iter dir holding the playwright screenshots
if (!OUT || !ITER) {
console.error('usage: node render-cards.mjs <out-dir> <iter-screenshots-dir>');
process.exit(2);
}
await fs.mkdir(OUT, { recursive: true });
const cream = '#fdf9ee';
const ink = '#1a1814';
const inkSoft = '#5a564d';
const coral = '#d9534f';
const dataUrl = async p => {
const buf = await fs.readFile(p);
return 'data:image/png;base64,' + buf.toString('base64');
};
const sxs = async (leftSrc, rightSrc, leftLbl, rightLbl, bgInk = false) => {
const bg = bgInk ? ink : cream;
const fg = bgInk ? cream : ink;
const [leftData, rightData] = await Promise.all([dataUrl(leftSrc), dataUrl(rightSrc)]);
return `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;height:720px;padding:16px;background:${bg};box-sizing:border-box;font-family:Helvetica,sans-serif">
<div style="display:flex;flex-direction:column;height:100%">
<div style="font-size:18px;color:${fg};opacity:0.7;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:8px">${leftLbl}</div>
<div style="flex:1;border-radius:6px;overflow:hidden;display:flex;align-items:flex-start;justify-content:center;background:${fg}">
<img src="${leftData}" style="width:100%;height:100%;object-fit:cover;object-position:top">
</div>
</div>
<div style="display:flex;flex-direction:column;height:100%">
<div style="font-size:18px;color:${fg};opacity:0.7;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:8px">${rightLbl}</div>
<div style="flex:1;border-radius:6px;overflow:hidden;display:flex;align-items:flex-start;justify-content:center;background:${fg}">
<img src="${rightData}" style="width:100%;height:100%;object-fit:cover;object-position:top">
</div>
</div>
</div>`;
};
const cards = [
{
name: 'title', bg: cream, fg: ink,
html: `
<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100vh;font-family:Georgia,serif">
<div style="font-size:88px;font-weight:700;letter-spacing:-0.02em;color:${ink}">lacountyeats</div>
<div style="font-size:28px;color:${inkSoft};margin-top:14px;font-style:italic;font-family:Helvetica,sans-serif">before & after — build 1</div>
</div>`,
},
{
name: 'sxs-light', bg: cream, fg: ink,
html: await sxs(`${ITER}/home-live-light.png`, `${ITER}/home-mockup-iter1-light.png`, 'BEFORE · production', 'AFTER · proposed iteration', false),
},
{
name: 'sxs-dark', bg: ink, fg: cream,
html: await sxs(`${ITER}/home-live-dark.png`, `${ITER}/home-mockup-iter1-dark.png`, 'BEFORE · production (dark)', 'AFTER · proposed (dark)', true),
},
{
name: 'findings', bg: cream, fg: ink,
html: `
<div style="padding:80px;font-family:Helvetica,sans-serif;line-height:1.5">
<div style="font-family:Georgia,serif;font-size:48px;font-weight:700;color:${ink};margin-bottom:48px">What changed</div>
<ol style="font-size:30px;color:${ink};padding-left:0;list-style:none;margin:0">
<li style="margin:18px 0">1. Dark-mode toggle (sun/moon) wired into header</li>
<li style="margin:18px 0">2. 4 new restaurants from press: Picala · Mitsi · Bengara · Sushi Nakazawa</li>
<li style="margin:18px 0">3. "Newest in LA" hero section on the homepage</li>
<li style="margin:18px 0;color:${coral};font-weight:600">4. Production bug fix: CSP unblocked Leaflet stylesheets on /map</li>
</ol>
<div style="font-size:22px;color:${inkSoft};margin-top:60px;font-style:italic">Queued for one Kamatera push — awaiting deploy intent</div>
</div>`,
},
{
name: 'outro', bg: ink, fg: cream,
html: `
<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100vh;font-family:Georgia,serif;color:${cream}">
<div style="font-size:72px;font-weight:700;letter-spacing:-0.02em">loop continues</div>
<div style="font-size:24px;color:${cream};opacity:0.65;margin-top:18px;font-family:Helvetica,sans-serif">every 25–35 min · iterations/SCOREBOARD.md</div>
</div>`,
},
];
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1 });
for (const c of cards) {
const page = await ctx.newPage();
await page.setContent(`<!doctype html><html><body style="margin:0;background:${c.bg};color:${c.fg}">${c.html}</body></html>`, { waitUntil: 'load' });
await page.screenshot({ path: path.join(OUT, `${c.name}.png`), type: 'png' });
await page.close();
console.log(`rendered ${c.name}.png`);
}
await browser.close();