import { spawnSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
const projectDir = process.cwd();
const chrome = process.env.CHROME_BIN || "/usr/bin/google-chrome";
const messages = readFileSync(resolve(projectDir, "ads.txt"), "utf8")
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
if (messages.length !== 35) {
throw new Error(`Expected 35 messages in ads.txt, found ${messages.length}.`);
}
const escapeHtml = (value) => value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
function formatCopy(message) {
const separator = message.indexOf("? ");
if (separator === -1) {
return { className: "copy", html: escapeHtml(message) };
}
const question = message.slice(0, separator + 1);
const answer = message.slice(separator + 2);
// Short Q&A concepts read as two distinct beats. Longer questions retain
// natural wrapping so the type never has to become smaller than the brand.
if (Math.max(question.length, answer.length) > 26) {
return { className: "copy", html: escapeHtml(message) };
}
return {
className: "copy qa",
html: `<span>${escapeHtml(question)}</span><span>${escapeHtml(answer)}</span>`,
};
}
function buildHtml(message, number) {
const copy = formatCopy(message);
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>After Dark Labs Ad ${number}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;700&display=swap" rel="stylesheet">
<style>
html {
background: #000;
}
body {
position: relative;
width: 100%;
max-width: 1000px;
aspect-ratio: 1;
margin: 0 auto;
padding: 36px;
box-sizing: border-box;
overflow: hidden;
background: #000;
color: #fff;
font-family: "Ubuntu", sans-serif;
}
.copy {
max-width: 400px;
margin: 0;
font-size: 64px;
font-weight: 400;
line-height: 1.15;
text-align: left;
}
.copy.qa {
max-width: calc(100% - 72px);
}
.copy.qa span {
display: block;
white-space: nowrap;
}
.brand {
position: absolute;
right: 40px;
bottom: 18px;
margin: 0;
font-size: 40px;
font-weight: 700;
line-height: 1;
text-align: right;
}
</style>
</head>
<body>
<h1 class="brand">AfterDarkLabs.io</h1>
<p class="${copy.className}">${copy.html}</p>
<script>
const copy = document.querySelector(".copy");
let fontSize = 64;
function copyFits() {
if (copy.classList.contains("qa")) {
const availableWidth = document.body.clientWidth - 72;
return [...copy.children].every((line) => line.scrollWidth <= availableWidth);
}
return copy.getBoundingClientRect().bottom <= 625;
}
function fitCopy() {
while (!copyFits() && fontSize > 44) {
fontSize -= 1;
copy.style.fontSize = fontSize + "px";
}
}
fitCopy();
document.fonts.ready.then(fitCopy);
</script>
</body>
</html>
`;
}
for (const [index, message] of messages.entries()) {
const number = index + 2;
const htmlPath = resolve(projectDir, `ad${number}.html`);
const pngPath = resolve(projectDir, `ad${number}.png`);
writeFileSync(htmlPath, buildHtml(message, number));
const result = spawnSync(chrome, [
"--headless=new",
"--disable-gpu",
"--hide-scrollbars",
"--force-device-scale-factor=1",
"--run-all-compositor-stages-before-draw",
"--virtual-time-budget=1000",
`--screenshot=${pngPath}`,
"--window-size=723,754",
pathToFileURL(htmlPath).href,
], { encoding: "utf8" });
if (result.status !== 0) {
throw new Error(`Chrome failed while rendering ad${number}.png:\n${result.stderr}`);
}
process.stdout.write(`Created ad${number}.html and ad${number}.png\n`);
}