71 lines
2.7 KiB
JavaScript
71 lines
2.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// Render each slide of a ThothII-style HTML deck to a high-res PNG.
|
|
// Assumes the deck exposes .slide sections navigable via #N hash routes.
|
|
//
|
|
// Usage:
|
|
// node render.mjs <deck.html> <outdir> [--hide "sel1,sel2"] [--scale 2] [--width 1600] [--height 900]
|
|
//
|
|
// Output: <outdir>/01.png .. NN.png + <outdir>/notes.json (slide number -> title text)
|
|
import { createRequire } from 'node:module';
|
|
import { execSync } from 'node:child_process';
|
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
function req(name) {
|
|
try { return require(name); } catch {}
|
|
const g = execSync('npm root -g').toString().trim();
|
|
return require(path.join(g, name));
|
|
}
|
|
|
|
const args = process.argv.slice(2);
|
|
const html = path.resolve(args[0]);
|
|
const outdir = path.resolve(args[1]);
|
|
if (!args[0] || !args[1]) {
|
|
console.error('usage: node render.mjs <deck.html> <outdir> [--hide "sel1,sel2"] [--scale 2] [--width 1600] [--height 900]');
|
|
process.exit(1);
|
|
}
|
|
const opt = (f, d) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : d; };
|
|
const hide = (opt('--hide', '') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
const scale = Number(opt('--scale', '2'));
|
|
const width = Number(opt('--width', '1600'));
|
|
const height = Number(opt('--height', '900'));
|
|
|
|
const { chromium } = req('playwright');
|
|
mkdirSync(outdir, { recursive: true });
|
|
|
|
const browser = await chromium.launch();
|
|
const page = await browser.newPage({
|
|
viewport: { width, height },
|
|
deviceScaleFactor: scale,
|
|
reducedMotion: 'reduce',
|
|
});
|
|
await page.goto('file://' + html, { waitUntil: 'networkidle' });
|
|
await page.evaluate(() => document.fonts.ready);
|
|
if (hide.length) {
|
|
await page.addStyleTag({ content: hide.map(s => `${s}{display:none!important}`).join('\n') });
|
|
}
|
|
|
|
const count = await page.locator('.slide').count();
|
|
const notes = {};
|
|
for (let i = 1; i <= count; i++) {
|
|
await page.evaluate(n => { location.hash = '#' + n; }, i);
|
|
await page.waitForFunction(n => {
|
|
const s = document.querySelectorAll('.slide')[n - 1];
|
|
return !!s && s.classList.contains('on');
|
|
}, i);
|
|
await page.waitForTimeout(450); // let the crossfade settle
|
|
const file = path.join(outdir, String(i).padStart(2, '0') + '.png');
|
|
await page.screenshot({ path: file });
|
|
notes[i] = await page.evaluate(n => {
|
|
const s = document.querySelectorAll('.slide')[n - 1];
|
|
const t = s.querySelector('.title, .cover-mark');
|
|
const txt = t ? t.innerText.trim().replace(/\s+/g, ' ') : '';
|
|
return txt || s.getAttribute('aria-label') || '';
|
|
}, i);
|
|
console.log('rendered', file);
|
|
}
|
|
await browser.close();
|
|
writeFileSync(path.join(outdir, 'notes.json'), JSON.stringify(notes, null, 2));
|
|
console.log('done:', count, 'slides ->', outdir);
|