Sync all skills and memories 2026-04-14 07:27
This commit is contained in:
179
skills/creative/p5js/scripts/export-frames.js
Executable file
179
skills/creative/p5js/scripts/export-frames.js
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* p5.js Skill — Headless Frame Export
|
||||
*
|
||||
* Captures frames from a p5.js sketch using Puppeteer (headless Chrome).
|
||||
* Uses noLoop() + redraw() for DETERMINISTIC frame-by-frame control.
|
||||
*
|
||||
* IMPORTANT: Your sketch must call noLoop() in setup() and set
|
||||
* window._p5Ready = true when initialized. This script calls redraw()
|
||||
* for each frame capture, ensuring exact 1:1 correspondence between
|
||||
* frameCount and captured frames.
|
||||
*
|
||||
* If the sketch does NOT set window._p5Ready, the script falls back to
|
||||
* a timed capture mode (less precise, may drop/duplicate frames).
|
||||
*
|
||||
* Usage:
|
||||
* node export-frames.js sketch.html [options]
|
||||
*
|
||||
* Options:
|
||||
* --output <dir> Output directory (default: ./frames)
|
||||
* --width <px> Canvas width (default: 1920)
|
||||
* --height <px> Canvas height (default: 1080)
|
||||
* --frames <n> Number of frames to capture (default: 1)
|
||||
* --fps <n> Target FPS for timed fallback mode (default: 30)
|
||||
* --wait <ms> Wait before first capture (default: 2000)
|
||||
* --selector <sel> Canvas CSS selector (default: canvas)
|
||||
*
|
||||
* Examples:
|
||||
* node export-frames.js sketch.html --frames 1 # single PNG
|
||||
* node export-frames.js sketch.html --frames 300 --fps 30 # 10s at 30fps
|
||||
* node export-frames.js sketch.html --width 3840 --height 2160 # 4K still
|
||||
*
|
||||
* Sketch template for deterministic capture:
|
||||
* function setup() {
|
||||
* createCanvas(1920, 1080);
|
||||
* pixelDensity(1);
|
||||
* noLoop(); // REQUIRED for deterministic capture
|
||||
* window._p5Ready = true; // REQUIRED to signal readiness
|
||||
* }
|
||||
* function draw() { ... }
|
||||
*/
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Parse CLI arguments
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const opts = {
|
||||
input: null,
|
||||
output: './frames',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
frames: 1,
|
||||
fps: 30,
|
||||
wait: 2000,
|
||||
selector: 'canvas',
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i].startsWith('--')) {
|
||||
const key = args[i].slice(2);
|
||||
const val = args[i + 1];
|
||||
if (key in opts && val !== undefined) {
|
||||
opts[key] = isNaN(Number(val)) ? val : Number(val);
|
||||
i++;
|
||||
}
|
||||
} else if (!opts.input) {
|
||||
opts.input = args[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts.input) {
|
||||
console.error('Usage: node export-frames.js <sketch.html> [options]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const opts = parseArgs();
|
||||
const inputPath = path.resolve(opts.input);
|
||||
|
||||
if (!fs.existsSync(inputPath)) {
|
||||
console.error(`File not found: ${inputPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
fs.mkdirSync(opts.output, { recursive: true });
|
||||
|
||||
console.log(`Capturing ${opts.frames} frame(s) from ${opts.input}`);
|
||||
console.log(`Resolution: ${opts.width}x${opts.height}`);
|
||||
console.log(`Output: ${opts.output}/`);
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-gpu',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-web-security',
|
||||
'--allow-file-access-from-files',
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setViewport({
|
||||
width: opts.width,
|
||||
height: opts.height,
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
|
||||
// Navigate to sketch
|
||||
const fileUrl = `file://${inputPath}`;
|
||||
await page.goto(fileUrl, { waitUntil: 'networkidle0', timeout: 30000 });
|
||||
|
||||
// Wait for canvas to appear
|
||||
await page.waitForSelector(opts.selector, { timeout: 10000 });
|
||||
|
||||
// Detect capture mode: deterministic (noLoop+redraw) vs timed (fallback)
|
||||
let deterministic = false;
|
||||
try {
|
||||
await page.waitForFunction('window._p5Ready === true', { timeout: 5000 });
|
||||
deterministic = true;
|
||||
console.log(`Mode: deterministic (noLoop + redraw)`);
|
||||
} catch {
|
||||
console.log(`Mode: timed fallback (sketch does not set window._p5Ready)`);
|
||||
console.log(` For frame-perfect capture, add noLoop() and window._p5Ready=true to setup()`);
|
||||
await new Promise(r => setTimeout(r, opts.wait));
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let i = 0; i < opts.frames; i++) {
|
||||
if (deterministic) {
|
||||
// Advance exactly one frame
|
||||
await page.evaluate(() => { redraw(); });
|
||||
// Brief settle time for render to complete
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
}
|
||||
|
||||
const frameName = `frame-${String(i).padStart(4, '0')}.png`;
|
||||
const framePath = path.join(opts.output, frameName);
|
||||
|
||||
// Capture the canvas element
|
||||
const canvas = await page.$(opts.selector);
|
||||
if (!canvas) {
|
||||
console.error('Canvas element not found');
|
||||
break;
|
||||
}
|
||||
|
||||
await canvas.screenshot({ path: framePath, type: 'png' });
|
||||
|
||||
// Progress
|
||||
if (i % 30 === 0 || i === opts.frames - 1) {
|
||||
const pct = ((i + 1) / opts.frames * 100).toFixed(1);
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
process.stdout.write(`\r Frame ${i + 1}/${opts.frames} (${pct}%) — ${elapsed}s`);
|
||||
}
|
||||
|
||||
// In timed mode, wait between frames
|
||||
if (!deterministic && i < opts.frames - 1) {
|
||||
await new Promise(r => setTimeout(r, 1000 / opts.fps));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n Done.');
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Error:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
108
skills/creative/p5js/scripts/render.sh
Executable file
108
skills/creative/p5js/scripts/render.sh
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
# p5.js Skill — Headless Render Pipeline
|
||||
# Renders a p5.js sketch to MP4 video via Puppeteer + ffmpeg
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/render.sh sketch.html output.mp4 [options]
|
||||
#
|
||||
# Options:
|
||||
# --width Canvas width (default: 1920)
|
||||
# --height Canvas height (default: 1080)
|
||||
# --fps Frames per second (default: 30)
|
||||
# --duration Duration in seconds (default: 10)
|
||||
# --quality CRF value 0-51 (default: 18, lower = better)
|
||||
# --frames-only Only export frames, skip MP4 encoding
|
||||
#
|
||||
# Examples:
|
||||
# bash scripts/render.sh sketch.html output.mp4
|
||||
# bash scripts/render.sh sketch.html output.mp4 --duration 30 --fps 60
|
||||
# bash scripts/render.sh sketch.html output.mp4 --width 3840 --height 2160
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Defaults
|
||||
WIDTH=1920
|
||||
HEIGHT=1080
|
||||
FPS=30
|
||||
DURATION=10
|
||||
CRF=18
|
||||
FRAMES_ONLY=false
|
||||
|
||||
# Parse arguments
|
||||
INPUT="${1:?Usage: render.sh <input.html> <output.mp4> [options]}"
|
||||
OUTPUT="${2:?Usage: render.sh <input.html> <output.mp4> [options]}"
|
||||
shift 2
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--width) WIDTH="$2"; shift 2 ;;
|
||||
--height) HEIGHT="$2"; shift 2 ;;
|
||||
--fps) FPS="$2"; shift 2 ;;
|
||||
--duration) DURATION="$2"; shift 2 ;;
|
||||
--quality) CRF="$2"; shift 2 ;;
|
||||
--frames-only) FRAMES_ONLY=true; shift ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
TOTAL_FRAMES=$((FPS * DURATION))
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
FRAME_DIR=$(mktemp -d)
|
||||
|
||||
echo "=== p5.js Render Pipeline ==="
|
||||
echo "Input: $INPUT"
|
||||
echo "Output: $OUTPUT"
|
||||
echo "Resolution: ${WIDTH}x${HEIGHT}"
|
||||
echo "FPS: $FPS"
|
||||
echo "Duration: ${DURATION}s (${TOTAL_FRAMES} frames)"
|
||||
echo "Quality: CRF $CRF"
|
||||
echo "Frame dir: $FRAME_DIR"
|
||||
echo ""
|
||||
|
||||
# Check dependencies
|
||||
command -v node >/dev/null 2>&1 || { echo "Error: Node.js required"; exit 1; }
|
||||
if [ "$FRAMES_ONLY" = false ]; then
|
||||
command -v ffmpeg >/dev/null 2>&1 || { echo "Error: ffmpeg required for MP4"; exit 1; }
|
||||
fi
|
||||
|
||||
# Step 1: Capture frames via Puppeteer
|
||||
echo "Step 1/2: Capturing ${TOTAL_FRAMES} frames..."
|
||||
node "$SCRIPT_DIR/export-frames.js" \
|
||||
"$INPUT" \
|
||||
--output "$FRAME_DIR" \
|
||||
--width "$WIDTH" \
|
||||
--height "$HEIGHT" \
|
||||
--frames "$TOTAL_FRAMES" \
|
||||
--fps "$FPS"
|
||||
|
||||
echo "Frames captured to $FRAME_DIR"
|
||||
|
||||
if [ "$FRAMES_ONLY" = true ]; then
|
||||
echo "Frames saved to: $FRAME_DIR"
|
||||
echo "To encode manually:"
|
||||
echo " ffmpeg -framerate $FPS -i $FRAME_DIR/frame-%04d.png -c:v libx264 -crf $CRF -pix_fmt yuv420p $OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Step 2: Encode to MP4
|
||||
echo "Step 2/2: Encoding MP4..."
|
||||
ffmpeg -y \
|
||||
-framerate "$FPS" \
|
||||
-i "$FRAME_DIR/frame-%04d.png" \
|
||||
-c:v libx264 \
|
||||
-preset slow \
|
||||
-crf "$CRF" \
|
||||
-pix_fmt yuv420p \
|
||||
-movflags +faststart \
|
||||
"$OUTPUT" \
|
||||
2>"$FRAME_DIR/ffmpeg.log"
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$FRAME_DIR"
|
||||
|
||||
# Report
|
||||
FILE_SIZE=$(ls -lh "$OUTPUT" | awk '{print $5}')
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo "Output: $OUTPUT ($FILE_SIZE)"
|
||||
echo "Duration: ${DURATION}s at ${FPS}fps, ${WIDTH}x${HEIGHT}"
|
||||
28
skills/creative/p5js/scripts/serve.sh
Executable file
28
skills/creative/p5js/scripts/serve.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# p5.js Skill — Local Development Server
|
||||
# Serves the current directory over HTTP for loading local assets (fonts, images)
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/serve.sh [port] [directory]
|
||||
#
|
||||
# Examples:
|
||||
# bash scripts/serve.sh # serve CWD on port 8080
|
||||
# bash scripts/serve.sh 3000 # serve CWD on port 3000
|
||||
# bash scripts/serve.sh 8080 ./my-project # serve specific directory
|
||||
|
||||
PORT="${1:-8080}"
|
||||
DIR="${2:-.}"
|
||||
|
||||
echo "=== p5.js Dev Server ==="
|
||||
echo "Serving: $(cd "$DIR" && pwd)"
|
||||
echo "URL: http://localhost:$PORT"
|
||||
echo "Press Ctrl+C to stop"
|
||||
echo ""
|
||||
|
||||
cd "$DIR" && python3 -m http.server "$PORT" 2>/dev/null || {
|
||||
echo "Python3 not found. Trying Node.js..."
|
||||
npx serve -l "$PORT" "$DIR" 2>/dev/null || {
|
||||
echo "Error: Need python3 or npx (Node.js) for local server"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
87
skills/creative/p5js/scripts/setup.sh
Executable file
87
skills/creative/p5js/scripts/setup.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
# p5.js Skill — Dependency Verification
|
||||
# Run: bash skills/creative/p5js/scripts/setup.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
fail() { echo -e "${RED}[FAIL]${NC} $1"; }
|
||||
|
||||
echo "=== p5.js Skill — Setup Check ==="
|
||||
echo ""
|
||||
|
||||
# Required: Node.js (for Puppeteer headless export)
|
||||
if command -v node &>/dev/null; then
|
||||
NODE_VER=$(node -v)
|
||||
ok "Node.js $NODE_VER"
|
||||
else
|
||||
warn "Node.js not found — optional, needed for headless export"
|
||||
echo " Install: https://nodejs.org/ or 'brew install node'"
|
||||
fi
|
||||
|
||||
# Required: npm (for Puppeteer install)
|
||||
if command -v npm &>/dev/null; then
|
||||
NPM_VER=$(npm -v)
|
||||
ok "npm $NPM_VER"
|
||||
else
|
||||
warn "npm not found — optional, needed for headless export"
|
||||
fi
|
||||
|
||||
# Optional: Puppeteer
|
||||
if node -e "require('puppeteer')" 2>/dev/null; then
|
||||
ok "Puppeteer installed"
|
||||
else
|
||||
warn "Puppeteer not installed — needed for headless export"
|
||||
echo " Install: npm install puppeteer"
|
||||
fi
|
||||
|
||||
# Optional: ffmpeg (for MP4 encoding from frame sequences)
|
||||
if command -v ffmpeg &>/dev/null; then
|
||||
FFMPEG_VER=$(ffmpeg -version 2>&1 | head -1 | awk '{print $3}')
|
||||
ok "ffmpeg $FFMPEG_VER"
|
||||
else
|
||||
warn "ffmpeg not found — needed for MP4 export"
|
||||
echo " Install: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"
|
||||
fi
|
||||
|
||||
# Optional: Python3 (for local server)
|
||||
if command -v python3 &>/dev/null; then
|
||||
PY_VER=$(python3 --version 2>&1 | awk '{print $2}')
|
||||
ok "Python $PY_VER (for local server: python3 -m http.server)"
|
||||
else
|
||||
warn "Python3 not found — needed for local file serving"
|
||||
fi
|
||||
|
||||
# Browser check (macOS)
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
if open -Ra "Google Chrome" 2>/dev/null; then
|
||||
ok "Google Chrome found"
|
||||
elif open -Ra "Safari" 2>/dev/null; then
|
||||
ok "Safari found"
|
||||
else
|
||||
warn "No browser detected"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Core Requirements ==="
|
||||
echo " A modern browser (Chrome/Firefox/Safari/Edge)"
|
||||
echo " p5.js loaded via CDN — no local install needed"
|
||||
echo ""
|
||||
echo "=== Optional (for export) ==="
|
||||
echo " Node.js + Puppeteer — headless frame capture"
|
||||
echo " ffmpeg — frame sequence to MP4"
|
||||
echo " Python3 — local development server"
|
||||
echo ""
|
||||
echo "=== Quick Start ==="
|
||||
echo " 1. Create an HTML file with inline p5.js sketch"
|
||||
echo " 2. Open in browser: open sketch.html"
|
||||
echo " 3. Press 's' to save PNG, 'g' to save GIF"
|
||||
echo ""
|
||||
echo "Setup check complete."
|
||||
Reference in New Issue
Block a user