SOURCE / PINNED RELEASE
Made of little things.
Powder Tool V600Billion
- Release
- 142767edcab8…
- Author-recorded commit
- 6d92971effd0…
- License
- LICENSE
- Author’s source reference
- nostr://npub1fllw8kw0thjj55wds0uugcnp5kej2nfxd36eruq39d56wwz8r44q5q78wj/wss%3A%2F%2Fgit.napplet.soy%2F/powder-toy
Archive hash verified: ed7d6a8ea7083197…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
/**
* The scene gallery's pictures: each scene runs in the built napplet
* (dist/index.html, through scripts/harness.mjs) for as long as its header
* says (`-- thumb: <seconds>`, default 6), optionally after one last line of
* Lua (`-- snap: <lua>`, e.g. a lightning bolt), and its simulation is saved
* half size, without the game's information in the corners, as
* src/scenes/thumbs/<id>.webp. Build first; the pictures go into
* the next build.
*
* node scripts/scene-thumbs.mjs [scene ids...]
*/
import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { startHarness } from './harness.mjs';
const SCENES = fileURLToPath(new URL('../src/scenes/', import.meta.url));
const THUMBS = `${SCENES}thumbs/`;
mkdirSync(THUMBS, { recursive: true });
const wanted = new Set(process.argv.slice(2));
const files = readdirSync(SCENES).filter((name) => name.endsWith('.lua')).sort();
const harness = await startHarness({ viewport: { width: 1280, height: 800 } });
try {
await harness.waitForGame();
await harness.frame().waitForFunction(() => window.powderUi, null, { timeout: 10000 });
for (const file of files) {
const id = file.replace(/^\d+-/, '').replace(/\.lua$/, '');
if (wanted.size && !wanted.has(id)) continue;
const source = readFileSync(`${SCENES}${file}`, 'utf8');
const header = (key) => new RegExp(`^--\\s*${key}:\\s*(.*)$`, 'm').exec(source)?.[1]?.trim();
const scene = await harness.frame().evaluate((sceneId) => window.powderUi.scenes.find((s) => s.id === sceneId), id);
if (!scene) throw new Error(`${id} is not in the built page; npm run build first`);
await harness.frame().evaluate((sceneId) => window.powderUi.loadScene(window.powderUi.scenes.find((s) => s.id === sceneId)), id);
await harness.tab.waitForTimeout(Number(header('thumb') ?? 6) * 1000);
// Without the game's information in the corners.
await harness.frame().evaluate(() => window.powderUi.lua('ren.hud(false)'));
const snap = header('snap');
if (snap) await harness.frame().evaluate((code) => window.powderUi.lua(code), snap);
await harness.tab.waitForTimeout(snap ? 34 : 50);
const png = await harness.frame().locator('#viewport').screenshot();
await harness.frame().evaluate(() => window.powderUi.lua('ren.hud(true)'));
const webp = await harness.tab.evaluate(async (base64) => {
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
const bitmap = await createImageBitmap(new Blob([bytes], { type: 'image/png' }));
const canvas = new OffscreenCanvas(306, 192);
const context = canvas.getContext('2d');
context.imageSmoothingQuality = 'high';
context.drawImage(bitmap, 0, 0, 306, 192);
const blob = await canvas.convertToBlob({ type: 'image/webp', quality: 0.8 });
const out = new Uint8Array(await blob.arrayBuffer());
let text = '';
for (const byte of out) text += String.fromCharCode(byte);
return btoa(text);
}, png.toString('base64'));
const bytes = Buffer.from(webp, 'base64');
writeFileSync(`${THUMBS}${id}.webp`, bytes);
console.log(`${id}: ${bytes.length} bytes`);
}
if (harness.errors.length) console.log(`page errors: ${harness.errors.join(' | ')}`);
} finally {
await harness.close();
}
