← 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.&nbsp; Dark-mode toggle (sun/moon) wired into header</li>
          <li style="margin:18px 0">2.&nbsp; 4 new restaurants from press: Picala · Mitsi · Bengara · Sushi Nakazawa</li>
          <li style="margin:18px 0">3.&nbsp; "Newest in LA" hero section on the homepage</li>
          <li style="margin:18px 0;color:${coral};font-weight:600">4.&nbsp; 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();