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.");