// The loading card's backdrop: the pond as it opens, with not a duck in it,
// shot at one device shape and squeezed into a progressive JPEG small enough to
// be down before the module is — the card blurs it anyway, so it wants to be
// cheap rather than sharp. Usage:
// node scripts/loader-shot.mjs [url] [out] [width] [height] [dpr] [outWidth] [quality]
// The raw PNG is kept under temp/ so the capture can be looked at unblurred.
// Needs `npm i --no-save sharp` — the only thing in the repo that uses it.
import puppeteer from 'puppeteer';
import sharp from 'sharp';
import { mkdirSync } from 'node:fs';
import { dirname, resolve, basename } from 'node:path';
const url = process.argv[2] || 'http://localhost:5180/';
const out = resolve(process.argv[3] || 'public/pond-loader.jpg');
const width = Number(process.argv[4]) || 430;
const height = Number(process.argv[5]) || 932;
const dpr = Number(process.argv[6]) || 2;
const outWidth = Number(process.argv[7]) || width;
const quality = Number(process.argv[8]) || 50;
const browser = await puppeteer.launch({
headless: true,
args: ['--enable-unsafe-swiftshader', '--hide-scrollbars'],
});
try {
const page = await browser.newPage();
await page.setViewport({ width, height, deviceScaleFactor: dpr, isMobile: dpr > 1, hasTouch: dpr > 1 });
page.on('pageerror', (err) => console.log(`[pageerror] ${err.message}`));
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
await page.waitForFunction('window.quack && window.quack.app.started', { timeout: 30000 });
// hold the pond at the pose it opens on, with the flock out of the shot, the
// odd feather or droplet out of it too, and the card itself out of the shot,
// then wait for the frame it is drawn in
await page.evaluate(async () => {
const q = window.quack;
q.app.paused = true;
document.getElementById('loading')?.remove();
for (const d of q.pond.ducks) d.root.visible = false;
for (const pool of [q.effects.feathers, q.effects.bits, q.effects.leaves, q.effects.snowflakes]) {
pool.mesh.visible = false;
}
for (const r of q.effects.ripples) r.mesh.visible = false;
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
});
const raw = resolve('temp/loader', basename(out).replace(/\.jpe?g$/i, '.png'));
mkdirSync(dirname(raw), { recursive: true });
await page.screenshot({ path: raw, type: 'png' });
const png = await sharp(raw);
const meta = await png.metadata();
mkdirSync(dirname(out), { recursive: true });
const info = await png
.resize({ width: outWidth, kernel: 'lanczos3' })
.jpeg({ quality, progressive: true, mozjpeg: true, chromaSubsampling: '4:2:0' })
.toFile(out);
console.log(
`${out} ${info.width}x${info.height} ${(info.size / 1024).toFixed(1)} kB ` +
`(from ${meta.width}x${meta.height} at ${dpr}x) raw: ${raw}`,
);
} finally {
await browser.close();
}