designpaca/apps/site/tools/capture-shots.mjs
Yun Chan e09d4efb1c
Some checks failed
ci / build (push) Has been cancelled
feat: strengthen design skill and refresh all showcases
2026-09-12 18:16:33 +09:00

194 lines
7.7 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import puppeteer from "puppeteer-core";
import {
collectPageFailures,
prepareStillPage,
resolveChrome,
routeForFile,
startPublicServer,
waitForStableVisual,
} from "./capture-stable.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.resolve(__dirname, "..");
const shotsDir = path.join(rootDir, "src", "assets", "shots");
const publicRoot = path.join(rootDir, "public");
function requestedSlugs(argv) {
const slugs = new Set();
for (let index = 2; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === "--slug") {
const slug = argv[index + 1];
if (!slug) throw new Error("--slug requires a showcase slug");
slugs.add(slug);
index += 1;
} else if (argument.startsWith("--slug=")) {
slugs.add(argument.slice("--slug=".length));
} else {
throw new Error(`Unknown argument: ${argument}`);
}
}
return slugs;
}
if (!fs.existsSync(shotsDir)) {
fs.mkdirSync(shotsDir, { recursive: true });
}
const targets = [
{
url: path.join(rootDir, "public", "work", "thursday-flowers", "index.html"),
out: path.join(shotsDir, "work-thursday-flowers.png"),
altUrl: path.join(rootDir, "public", "work", "thursday-flowers", "index.html"),
altOut: path.join(shotsDir, "work-thursday-flowers-2.png"),
},
{
url: path.join(rootDir, "public", "work", "muwol-coffee", "index.html"),
out: path.join(shotsDir, "work-muwol-coffee.png"),
altUrl: path.join(rootDir, "public", "work", "muwol-coffee", "index.html"),
altOut: path.join(shotsDir, "work-muwol-coffee-2.png"),
},
{
url: path.join(rootDir, "public", "work", "pulsegate", "index.html"),
out: path.join(shotsDir, "work-pulsegate.png"),
altUrl: path.join(rootDir, "public", "work", "pulsegate", "index.html"),
altOut: path.join(shotsDir, "work-pulsegate-2.png"),
},
{
url: path.join(rootDir, "public", "work", "form-architecture", "index.html"),
out: path.join(shotsDir, "work-form-architecture.png"),
altUrl: path.join(rootDir, "public", "work", "form-architecture", "index.html"),
altOut: path.join(shotsDir, "work-form-architecture-2.png"),
},
{
url: path.join(rootDir, "public", "work", "overtone-sound", "index.html"),
out: path.join(shotsDir, "work-overtone-sound.png"),
altUrl: path.join(rootDir, "public", "work", "overtone-sound", "index.html"),
altOut: path.join(shotsDir, "work-overtone-sound-2.png"),
},
{
url: path.join(rootDir, "public", "work", "font-foundry", "index.html"),
out: path.join(shotsDir, "work-font-foundry.png"),
altUrl: path.join(rootDir, "public", "work", "font-foundry", "index.html"),
altOut: path.join(shotsDir, "work-font-foundry-2.png"),
},
{
url: path.join(rootDir, "public", "work", "lumina-optics", "index.html"),
out: path.join(shotsDir, "work-lumina-optics.png"),
altUrl: path.join(rootDir, "public", "work", "lumina-optics", "index.html"),
altOut: path.join(shotsDir, "work-lumina-optics-2.png"),
webgl: true, // three.js 캔버스 — swiftshader 필요
},
{
url: path.join(rootDir, "public", "work", "hyang-incense", "index.html"),
out: path.join(shotsDir, "work-hyang-incense.png"),
altUrl: path.join(rootDir, "public", "work", "hyang-incense", "index.html"),
altOut: path.join(shotsDir, "work-hyang-incense-2.png"),
},
{
url: path.join(rootDir, "public", "work", "synapse-bci", "index.html"),
out: path.join(shotsDir, "work-synapse-bci.png"),
altUrl: path.join(rootDir, "public", "work", "synapse-bci", "index.html"),
altOut: path.join(shotsDir, "work-synapse-bci-2.png"),
},
{
url: path.join(rootDir, "public", "work", "baekje-celadon", "index.html"),
out: path.join(shotsDir, "work-baekje-celadon.png"),
altUrl: path.join(rootDir, "public", "work", "baekje-celadon", "index.html"),
altOut: path.join(shotsDir, "work-baekje-celadon-2.png"),
},
{
url: path.join(rootDir, "public", "work", "orbit-aerospace", "index.html"),
out: path.join(shotsDir, "work-orbit-aerospace.png"),
altUrl: path.join(rootDir, "public", "work", "orbit-aerospace", "index.html"),
altOut: path.join(shotsDir, "work-orbit-aerospace-2.png"),
},
{
url: path.join(rootDir, "public", "work", "gaon-lms", "index.html"),
out: path.join(shotsDir, "work-gaon-lms.png"),
altUrl: path.join(rootDir, "public", "work", "gaon-lms", "index.html"),
altOut: path.join(shotsDir, "work-gaon-lms-2.png"),
},
{
url: path.join(rootDir, "public", "work", "dure-enrollment", "index.html"),
out: path.join(shotsDir, "work-dure-enrollment.png"),
altUrl: path.join(rootDir, "public", "work", "dure-enrollment", "index.html"),
altOut: path.join(shotsDir, "work-dure-enrollment-2.png"),
},
{
url: path.join(rootDir, "public", "work", "cheongon-care", "index.html"),
out: path.join(shotsDir, "work-cheongon-care.png"),
altUrl: path.join(rootDir, "public", "work", "cheongon-care", "protocol", "index.html"),
altOut: path.join(shotsDir, "work-cheongon-care-2.png"),
},
{
url: path.join(rootDir, "public", "work", "cask-archive", "index.html"),
out: path.join(shotsDir, "work-cask-archive.png"),
altUrl: path.join(rootDir, "public", "work", "cask-archive", "docent", "index.html"),
altOut: path.join(shotsDir, "work-cask-archive-2.png"),
},
{
url: path.join(rootDir, "public", "work", "materia-interior", "index.html"),
out: path.join(shotsDir, "work-materia-interior.png"),
altUrl: path.join(rootDir, "public", "work", "materia-interior", "projects", "hannam-duplex", "index.html"),
altOut: path.join(shotsDir, "work-materia-interior-2.png"),
},
];
const requested = requestedSlugs(process.argv);
const slugOf = (target) => path.basename(path.dirname(target.url));
const known = new Set(targets.map(slugOf));
const unknown = [...requested].filter((slug) => !known.has(slug));
if (unknown.length) throw new Error(`Unknown showcase slug(s): ${unknown.join(", ")}`);
const selectedTargets = requested.size ? targets.filter((target) => requested.has(slugOf(target))) : targets;
console.log("📸 Capturing settled, high-resolution screenshots...");
const server = await startPublicServer(publicRoot);
let browser;
async function shot(file, output) {
const page = await browser.newPage();
const failures = collectPageFailures(page, server.origin);
try {
await prepareStillPage(page);
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
const route = routeForFile(publicRoot, file);
const response = await page.goto(server.origin + route, { waitUntil: "domcontentloaded", timeout: 30000 });
if (!response?.ok()) throw new Error(`${route} returned ${response?.status() ?? "no response"}`);
await waitForStableVisual(page);
if (failures.length) throw new Error(failures.join(" · "));
await page.screenshot({ path: output, type: "png", fullPage: false });
const size = Math.round(fs.statSync(output).size / 1024);
console.log(`CAPTURE ${path.basename(output)} (${size}KB)`);
} finally {
await page.close();
}
}
try {
browser = await puppeteer.launch({
executablePath: resolveChrome(),
headless: "new",
args: [
"--no-sandbox",
"--use-angle=swiftshader",
"--enable-unsafe-swiftshader",
"--force-device-scale-factor=1",
],
defaultViewport: { width: 1440, height: 900, deviceScaleFactor: 1 },
});
for (const target of selectedTargets) {
await shot(target.url, target.out);
if (target.altOut) await shot(target.altUrl, target.altOut);
}
} finally {
if (browser) await browser.close();
await server.close();
}
console.log("✅ All requested screenshots captured after visual settling.");