← Back to SKILL.md1#!/usr/bin/env node2/**3 * p5.js Skill — Headless Frame Export4 *5 * Captures frames from a p5.js sketch using Puppeteer (headless Chrome).6 * Uses noLoop() + redraw() for DETERMINISTIC frame-by-frame control.7 *8 * IMPORTANT: Your sketch must call noLoop() in setup() and set9 * window._p5Ready = true when initialized. This script calls redraw()10 * for each frame capture, ensuring exact 1:1 correspondence between11 * frameCount and captured frames.12 *13 * If the sketch does NOT set window._p5Ready, the script falls back to14 * a timed capture mode (less precise, may drop/duplicate frames).15 *16 * Usage:17 * node export-frames.js sketch.html [options]18 *19 * Options:20 * --output <dir> Output directory (default: ./frames)21 * --width <px> Canvas width (default: 1920)22 * --height <px> Canvas height (default: 1080)23 * --frames <n> Number of frames to capture (default: 1)24 * --fps <n> Target FPS for timed fallback mode (default: 30)25 * --wait <ms> Wait before first capture (default: 2000)26 * --selector <sel> Canvas CSS selector (default: canvas)27 *28 * Examples:29 * node export-frames.js sketch.html --frames 1 # single PNG30 * node export-frames.js sketch.html --frames 300 --fps 30 # 10s at 30fps31 * node export-frames.js sketch.html --width 3840 --height 2160 # 4K still32 *33 * Sketch template for deterministic capture:34 * function setup() {35 * createCanvas(1920, 1080);36 * pixelDensity(1);37 * noLoop(); // REQUIRED for deterministic capture38 * window._p5Ready = true; // REQUIRED to signal readiness39 * }40 * function draw() { ... }41 */42 43const puppeteer = require('puppeteer');44const path = require('path');45const fs = require('fs');46 47// Parse CLI arguments48function parseArgs() {49 const args = process.argv.slice(2);50 const opts = {51 input: null,52 output: './frames',53 width: 1920,54 height: 1080,55 frames: 1,56 fps: 30,57 wait: 2000,58 selector: 'canvas',59 };60 61 for (let i = 0; i < args.length; i++) {62 if (args[i].startsWith('--')) {63 const key = args[i].slice(2);64 const val = args[i + 1];65 if (key in opts && val !== undefined) {66 opts[key] = isNaN(Number(val)) ? val : Number(val);67 i++;68 }69 } else if (!opts.input) {70 opts.input = args[i];71 }72 }73 74 if (!opts.input) {75 console.error('Usage: node export-frames.js <sketch.html> [options]');76 process.exit(1);77 }78 79 return opts;80}81 82async function main() {83 const opts = parseArgs();84 const inputPath = path.resolve(opts.input);85 86 if (!fs.existsSync(inputPath)) {87 console.error(`File not found: ${inputPath}`);88 process.exit(1);89 }90 91 // Create output directory92 fs.mkdirSync(opts.output, { recursive: true });93 94 console.log(`Capturing ${opts.frames} frame(s) from ${opts.input}`);95 console.log(`Resolution: ${opts.width}x${opts.height}`);96 console.log(`Output: ${opts.output}/`);97 98 const browser = await puppeteer.launch({99 headless: 'new',100 args: [101 '--no-sandbox',102 '--disable-setuid-sandbox',103 '--disable-gpu',104 '--disable-dev-shm-usage',105 '--disable-web-security',106 '--allow-file-access-from-files',107 ],108 });109 110 const page = await browser.newPage();111 112 await page.setViewport({113 width: opts.width,114 height: opts.height,115 deviceScaleFactor: 1,116 });117 118 // Navigate to sketch119 const fileUrl = `file://${inputPath}`;120 await page.goto(fileUrl, { waitUntil: 'networkidle0', timeout: 30000 });121 122 // Wait for canvas to appear123 await page.waitForSelector(opts.selector, { timeout: 10000 });124 125 // Detect capture mode: deterministic (noLoop+redraw) vs timed (fallback)126 let deterministic = false;127 try {128 await page.waitForFunction('window._p5Ready === true', { timeout: 5000 });129 deterministic = true;130 console.log(`Mode: deterministic (noLoop + redraw)`);131 } catch {132 console.log(`Mode: timed fallback (sketch does not set window._p5Ready)`);133 console.log(` For frame-perfect capture, add noLoop() and window._p5Ready=true to setup()`);134 await new Promise(r => setTimeout(r, opts.wait));135 }136 137 const startTime = Date.now();138 139 for (let i = 0; i < opts.frames; i++) {140 if (deterministic) {141 // Advance exactly one frame142 await page.evaluate(() => { redraw(); });143 // Brief settle time for render to complete144 await new Promise(r => setTimeout(r, 20));145 }146 147 const frameName = `frame-${String(i).padStart(4, '0')}.png`;148 const framePath = path.join(opts.output, frameName);149 150 // Capture the canvas element151 const canvas = await page.$(opts.selector);152 if (!canvas) {153 console.error('Canvas element not found');154 break;155 }156 157 await canvas.screenshot({ path: framePath, type: 'png' });158 159 // Progress160 if (i % 30 === 0 || i === opts.frames - 1) {161 const pct = ((i + 1) / opts.frames * 100).toFixed(1);162 const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);163 process.stdout.write(`\r Frame ${i + 1}/${opts.frames} (${pct}%) — ${elapsed}s`);164 }165 166 // In timed mode, wait between frames167 if (!deterministic && i < opts.frames - 1) {168 await new Promise(r => setTimeout(r, 1000 / opts.fps));169 }170 }171 172 console.log('\n Done.');173 await browser.close();174}175 176main().catch(err => {177 console.error('Error:', err.message);178 process.exit(1);179});180