Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580#!/usr/bin/env node
/** * Stateless Frame Renderer * Renders exactly one frame then exits * Can resume from any point by reading state.json */
import fs from "fs";import path from "path";import { fileURLToPath } from "url";import { HeadlessAC } from "./headless.mjs";
const __filename = fileURLToPath(import.meta.url);const __dirname = path.dirname(__filename);
class FrameRenderer extends HeadlessAC { constructor(outputDir, width = 2048, height = 2048, density = null, fps = 60) { // Use provided dimensions or fall back to environment variables width = width || parseInt(process.env.WIDTH) || 2048; height = height || parseInt(process.env.HEIGHT) || 2048; super(width, height, { density, outputDir }); this.outputDir = outputDir; this.stateFile = path.join(outputDir, 'state.json'); this.backgroundBufferFile = path.join(outputDir, 'background-buffer.bin'); this.density = density; this.fps = fps; // State containers for serialization/deserialization this.pieceState = {}; // Variables from the piece itself this.storeData = {}; // AC store data this.kidlispState = {}; // KidLisp instance state including onceExecuted this.pieceModule = null; // Cache the piece module this.diskState = null; // State from AC disk lifecycle this.backgroundBuffer = null; // Previous frame's pixel buffer for continuity }
// Load or create initial state loadState() { if (fs.existsSync(this.stateFile)) { console.log(`๐ Resuming from existing state`); const state = JSON.parse(fs.readFileSync(this.stateFile, 'utf8')); // Restore piece state if it exists if (state.pieceState) { console.log(`๐งฉ Restoring piece state with ${Object.keys(state.pieceState).length} variables`); this.pieceState = state.pieceState; } // Restore AC store data if it exists if (state.storeData) { console.log(`๐พ Restoring store data with ${Object.keys(state.storeData).length} entries`); this.storeData = state.storeData; } // Restore KidLisp state if it exists if (state.kidlispState) { console.log(`๐จ Restoring KidLisp state with ${Object.keys(state.kidlispState).length} properties`); this.kidlispState = state.kidlispState; }
// Restore deterministic random generator state if available if (state.randomState) { console.log(`๐ฒ Restoring random state (seed=${state.randomState.seed}, sequence=${state.randomState.sequence})`); this.setRandomState(state.randomState); } // Note: Background buffer will be loaded in renderFrame after canvas is sized return state; } else { console.log(`๐ Starting new render sequence`); const pieceArg = process.argv[2] || 'test-piece.mjs'; const totalFrames = parseInt(process.argv[3]) || 300; // Default to 300 frames if not provided return { frameIndex: 0, startTime: Date.now(), piece: pieceArg, duration: totalFrames * (1000/this.fps), // Convert frames to milliseconds for compatibility fps: this.fps, width: this.width, height: this.height, totalFrames: totalFrames, pieceState: {}, storeData: {}, kidlispState: {} }; } }
// Load background buffer from previous frame loadBackgroundBuffer() { this.backgroundBufferRestored = false; // Reset flag if (fs.existsSync(this.backgroundBufferFile)) { console.log(`๐ผ๏ธ Restoring background buffer from previous frame`); try { const startTime = Date.now(); const bufferData = fs.readFileSync(this.backgroundBufferFile); // Ensure the buffer is the right size if (bufferData.length === this.pixelBuffer.length) { this.pixelBuffer.set(bufferData); this.backgroundBufferRestored = true; // Set flag when successfully restored const loadTime = Date.now() - startTime; console.log(`โ
Background buffer restored (${bufferData.length} bytes in ${loadTime}ms)`); console.log(`๐งช Background buffer preview (first 16 bytes):`, Array.from(this.pixelBuffer.slice(0, 16))); } else { console.log(`โ ๏ธ Background buffer size mismatch: expected ${this.pixelBuffer.length}, got ${bufferData.length}`); } } catch (error) { console.log(`โ ๏ธ Failed to load background buffer: ${error.message}`); } } }
// Save background buffer for next frame saveBackgroundBuffer() { try { const startTime = Date.now(); console.log(`๐งช Background buffer sample before save (first 16 bytes):`, Array.from(this.pixelBuffer.slice(0, 16))); fs.writeFileSync(this.backgroundBufferFile, this.pixelBuffer); const saveTime = Date.now() - startTime; console.log(`๐พ Background buffer saved for frame continuity (${saveTime}ms)`); } catch (error) { console.log(`โ ๏ธ Failed to save background buffer: ${error.message}`); } }
// Save state for next frame saveState(state) { // Include piece state, store data, and KidLisp state in the state object const completeState = { ...state, pieceState: this.pieceState || {}, storeData: this.storeData || {}, kidlispState: this.getKidlispState(), randomState: this.getRandomState() }; fs.writeFileSync(this.stateFile, JSON.stringify(completeState, null, 2)); }
// Check if rendering is complete isComplete(state) { return state.frameIndex >= state.totalFrames; }
// Render one frame using the existing headless system async renderFrame(state) { const frameStartTime = Date.now(); console.log(`๐ฌ Rendering frame ${state.frameIndex}/${state.totalFrames}`); // Reset first-line color tracking for this frame this.firstLineColorApplied = false;
// Calculate frame time const frameTimeMs = (state.frameIndex / state.fps) * 1000;
// Keep frame counters synchronized for downstream helpers this.frameCount = state.frameIndex; // Set the simulation time for timing-based expressions this.setSimulationTime(frameTimeMs); // Update canvas size if needed if (this.width !== state.width || this.height !== state.height) { console.log(`๐ผ๏ธ Resizing canvas from ${this.width}x${this.height} to ${state.width}x${state.height}`); this.width = state.width; this.height = state.height; this.pixelBuffer = new Uint8Array(state.width * state.height * 4); // Initialize to black if this is a new canvas size for (let i = 0; i < this.pixelBuffer.length; i += 4) { this.pixelBuffer[i] = 0; // R this.pixelBuffer[i + 1] = 0; // G this.pixelBuffer[i + 2] = 0; // B this.pixelBuffer[i + 3] = 255; // A (opaque) } // Force API recreation after resize to get correct screen dimensions this.api = null; } // Load background buffer from previous frame for continuity (only after frame 0) let backgroundLoadTime = 0; if (state.frameIndex > 0) { const bgStartTime = Date.now(); this.loadBackgroundBuffer(); backgroundLoadTime = Date.now() - bgStartTime; // CRITICAL: Update graph buffer reference after loading background buffer // The graph.mjs system needs to point to the updated pixelBuffer with restored pixels if (this.graph && this.graph.setBuffer) { const buffer = { width: this.width, height: this.height, pixels: this.pixelBuffer // This now contains the restored background pixels }; this.graph.setBuffer(buffer); console.log('๐จ Updated graph buffer reference with restored background pixels'); } } // Initialize AC system if not already done let acInitTime = 0; let apiCreateTime = 0; if (!this.api) { const acStartTime = Date.now(); await this.initializeAC(); acInitTime = Date.now() - acStartTime; // Set KidLisp state BEFORE creating API so it's available when KidLisp instance is created if (state.kidlispState) { this.setKidlispState(state.kidlispState); console.log('๐ Pre-set KidLisp state before API creation'); } const apiStartTime = Date.now(); this.api = await this.createAPI(); // Create the API after initialization - screen.pixels will now reference the loaded background buffer apiCreateTime = Date.now() - apiStartTime; if (this.kidlispInstance) { console.log(`๐ฏ KidLisp instance established at frame ${state.frameIndex}`); // CRITICAL: Ensure simulation time is properly set for KidLisp timing calculations // Force update the time after API creation to ensure consistent timing this.setSimulationTime(frameTimeMs); console.log(`๐ Re-synchronized simulation time after API creation: ${frameTimeMs}ms`); } }
// Keep KidLisp timing counters aligned with renderer frame index if (this.kidlispInstance) { this.kidlispInstance.frameCount = state.frameIndex; } // Load the piece module if not cached let pieceLoadTime = 0; if (!this.pieceModule) { const pieceStartTime = Date.now(); // Check if this is a KidLisp code (starts with $) console.log(`๐ Debug: piece="${state.piece}", GIF_MODE=${process.env.GIF_MODE}`); if (state.piece.startsWith('$') && process.env.GIF_MODE) { console.log(`๐ค KidLisp code detected: ${state.piece}`); // Check if temp file already exists (for subsequent frames) const tempPath = path.join(this.outputDir, 'temp-kidlisp.mjs'); if (fs.existsSync(tempPath)) { console.log(`๐ Reusing existing KidLisp wrapper`); this.pieceModule = await import(path.resolve(tempPath) + '?t=' + Date.now()); } else { // Create a temporary wrapper piece for KidLisp code const kidlispWrapper = `// Temporary KidLisp wrapper for ${state.piece}export const paint = ($) => { // TODO: Resolve KidLisp code and execute it // For now, create a simple animated visual const t = $.api.frame / ${state.fps}; $.wipe('purple'); $.ink('white'); $.line(200 + Math.sin(t) * 100, 200, 200 + Math.cos(t) * 100, 200);};`; // Write temporary file fs.writeFileSync(tempPath, kidlispWrapper); this.pieceModule = await import(path.resolve(tempPath)); console.log(`๐ค Created KidLisp wrapper: ${Object.keys(this.pieceModule).join(', ')}`); } } else { // Try to import the piece - handle both with and without .mjs extension let piecePath = state.piece; if (!piecePath.endsWith('.mjs') && !piecePath.endsWith('.js')) { piecePath = `${piecePath}.mjs`; } // If the path is not absolute, try to resolve it relative to the disks directory let resolvedPath = piecePath; if (!path.isAbsolute(piecePath)) { // First, try as an absolute path if (fs.existsSync(path.resolve(piecePath))) { resolvedPath = path.resolve(piecePath); } else { // Otherwise, try in the disks directory const disksDir = path.join(__dirname, '..', '..', '..', 'system', 'public', 'aesthetic.computer', 'disks'); const disksPath = path.join(disksDir, piecePath); if (fs.existsSync(disksPath)) { resolvedPath = disksPath; console.log(`๐ Found piece in disks directory: ${disksPath}`); } else { // Fallback to original resolution resolvedPath = path.resolve(piecePath); console.log(`โ ๏ธ Piece not found in disks directory, using: ${resolvedPath}`); } } } this.pieceModule = await import(resolvedPath); console.log(`๐ฆ Loaded piece with functions: ${Object.keys(this.pieceModule).join(', ')}`); } pieceLoadTime = Date.now() - pieceStartTime; } // Load and run the piece let pieceExecutionTime = 0; try { const pieceExecStartTime = Date.now(); // Restore piece state to global scope (for pieces that use global variables) if (state.pieceState) { Object.assign(global, state.pieceState); console.log(`๐งฉ Restored ${Object.keys(state.pieceState).length} piece variables to global scope`); } // Create enhanced API with state persistence and frame injection const enhancedAPI = { ...this.api, // Frame context for pieces (both frameIndex and frame for compatibility) frameIndex: state.frameIndex, frame: state.frameIndex, // KidLisp expects api.frame paintCount: state.frameIndex, // KidLisp frame function expects api.paintCount frameTime: frameTimeMs, totalFrames: state.totalFrames, // Store system for persistence store: { ...this.storeData, persist: (key, method = "memory") => { // Store in our state system console.log(`๐พ Persisting ${key} to ${method}`); } }, // Simulation count (for pieces that depend on it) simCount: BigInt(state.frameIndex), // Add common injection patterns $: this.api // Common alias };
// Create piece parameters object (for pieces that expect destructured params) const pieceParams = { api: enhancedAPI, frameIndex: state.frameIndex, frameTime: frameTimeMs, totalFrames: state.totalFrames, simCount: BigInt(state.frameIndex) }; // Clear the canvas (only for frame 0 - background persistence preserves previous frames) if (state.frameIndex === 0) { this.pixelBuffer.fill(0); for (let i = 3; i < this.pixelBuffer.length; i += 4) { this.pixelBuffer[i] = 255; // Alpha } console.log('๐จ Canvas cleared for frame 0'); } else { console.log('๐จ Preserving background buffer for frame continuity'); } // Handle AC disk structure (boot/sim/paint) vs simple paint function if (this.pieceModule.boot && this.pieceModule.paint) { console.log('๐ง AC disk detected - running boot/sim/paint lifecycle...'); // Check if this is a KidLisp wrapper piece (has kidlisp() call in paint) // For KidLisp pieces, we DON'T want to call boot on every frame as it may // interfere with embedded layer state restoration const isKidlispWrapper = state.piece.includes('-kidlisp.mjs') || this.pieceModule.paint.toString().includes('api.kidlisp'); if (isKidlispWrapper) { // For KidLisp wrappers, only call boot on frame 0 // Embedded layer state is managed by the KidLisp system itself if (state.frameIndex === 0) { console.log('๐ฅพ Running boot function (KidLisp wrapper, frame 0 only)...'); await this.pieceModule.boot(enhancedAPI); } else { console.log('โญ๏ธ Skipping boot for KidLisp wrapper on frame ${state.frameIndex}'); } } else { // For regular AC disks with module-level state, call boot on every frame // This is necessary because each frame runs in a fresh Node.js process console.log('๐ฅพ Running boot function (regular AC disk)...'); await this.pieceModule.boot(enhancedAPI); } // Run sim function if it exists if (this.pieceModule.sim) { await this.pieceModule.sim(enhancedAPI, { frameIndex: state.frameIndex, frameTime: frameTimeMs, totalFrames: state.totalFrames, simCount: BigInt(state.frameIndex) }); } // Run paint function await this.pieceModule.paint(enhancedAPI, { frameIndex: state.frameIndex, frameTime: frameTimeMs, totalFrames: state.totalFrames, simCount: BigInt(state.frameIndex) }); } else if (this.pieceModule.paint) { // Standard paint function only await this.pieceModule.paint(enhancedAPI, { frameIndex: state.frameIndex, frameTime: frameTimeMs, totalFrames: state.totalFrames, simCount: BigInt(state.frameIndex) }); } else if (this.pieceModule.default && typeof this.pieceModule.default === 'function') { // Default export function await this.pieceModule.default(enhancedAPI, { frameIndex: state.frameIndex, frameTime: frameTimeMs, totalFrames: state.totalFrames, simCount: BigInt(state.frameIndex) }); } else { throw new Error(`Piece ${state.piece} does not export valid AC disk structure or paint function`); } // Capture piece state from global scope after execution const capturedState = {}; // Common piece variables to capture const commonStateVars = [ 'frameCount', 'marqueeOffset', 'currentFrame', 'animationState', 'time', 'progress', 'phase', 'cycle', 'step', 'counter', 'index', 'x', 'y', 'vx', 'vy', 'angle', 'rotation', 'scale', 'opacity', 'colors', 'positions', 'velocities', 'particles', 'objects', 'gameState', 'score', 'level', 'lives', 'health', 'energy', 'stars', 'bouncingStars' // elcid-flyer specific state ]; // Capture variables that exist in global scope commonStateVars.forEach(varName => { if (typeof global[varName] !== 'undefined') { capturedState[varName] = global[varName]; } }); // Store captured state this.pieceState = capturedState; // Capture store data that was modified this.storeData = { ...enhancedAPI.store }; pieceExecutionTime = Date.now() - pieceExecStartTime; console.log(`๐ Captured ${Object.keys(capturedState).length} piece variables`); // CRITICAL: Sync API screen buffer back to our pixel buffer BEFORE capturing frame if (this.api && this.api.screen && this.api.screen.pixels) { console.log('๐ Syncing API screen buffer to pixel buffer for persistence'); // Debug: Check a few pixels to see if there's actual content const samplePixels = []; for (let i = 0; i < Math.min(20, this.api.screen.pixels.length); i += 4) { samplePixels.push(`[${this.api.screen.pixels[i]},${this.api.screen.pixels[i+1]},${this.api.screen.pixels[i+2]},${this.api.screen.pixels[i+3]}]`); } console.log('๐ API screen buffer sample pixels:', samplePixels.slice(0, 5).join(', ')); // Also check if graph buffer has different content if (this.graph && this.graph.pixels) { const graphSamplePixels = []; for (let i = 0; i < Math.min(20, this.graph.pixels.length); i += 4) { graphSamplePixels.push(`[${this.graph.pixels[i]},${this.graph.pixels[i+1]},${this.graph.pixels[i+2]},${this.graph.pixels[i+3]}]`); } console.log('๐ Graph buffer sample pixels:', graphSamplePixels.slice(0, 5).join(', ')); } this.pixelBuffer.set(this.api.screen.pixels); } // Capture the frame AFTER syncing the screen buffer const frameData = this.captureFrame(); // Convert RGBA to RGB for ffmpeg const rgbData = new Uint8Array(state.width * state.height * 3); for (let i = 0; i < frameData.length; i += 4) { const rgbIndex = (i / 4) * 3; rgbData[rgbIndex] = frameData[i]; // R rgbData[rgbIndex + 1] = frameData[i + 1]; // G rgbData[rgbIndex + 2] = frameData[i + 2]; // B // Skip alpha channel } // Write frame file const fileWriteStartTime = Date.now(); const frameFile = path.join(this.outputDir, `frame-${String(state.frameIndex).padStart(6, '0')}.rgb`); fs.writeFileSync(frameFile, rgbData); const fileWriteTime = Date.now() - fileWriteStartTime; console.log(`โ
Frame ${state.frameIndex} written (${rgbData.length} bytes)`); // Save background buffer for next frame continuity this.saveBackgroundBuffer(); // Performance summary const totalFrameTime = Date.now() - frameStartTime; console.log(`โฑ๏ธ FRAME ${state.frameIndex} TIMING: Total=${totalFrameTime}ms | BG=${backgroundLoadTime}ms | AC=${acInitTime}ms | API=${apiCreateTime}ms | Piece=${pieceLoadTime}ms | Exec=${pieceExecutionTime}ms | File=${fileWriteTime}ms`); return { ...state, frameIndex: state.frameIndex + 1, lastFrameTime: frameTimeMs, pieceState: this.pieceState, storeData: this.storeData }; } catch (error) { console.error(`๐ฅ Frame ${state.frameIndex} render failed:`, error.message); throw error; } }
// Capture frame method (inherited from HeadlessAC but make sure it exists) captureFrame() { return new Uint8Array(this.pixelBuffer); }
async run() { try { // Ensure output directory exists fs.mkdirSync(this.outputDir, { recursive: true }); // Load current state const currentState = this.loadState(); // Check if complete if (this.isComplete(currentState)) { console.log(`๐ฏ Rendering complete! ${currentState.frameIndex} frames rendered.`); return { complete: true, state: currentState }; } // Render exactly one frame const newState = await this.renderFrame(currentState); // Save state for next invocation this.saveState(newState); console.log(`๐ State saved. Next frame: ${newState.frameIndex}`); return { complete: this.isComplete(newState), state: newState }; } catch (error) { console.error(`๐ฅ Frame render failed:`, error); process.exit(1); } }}
// CLI usageif (import.meta.url === `file://${process.argv[1]}`) { const outputDir = process.argv[4] || './frame-output'; const width = parseInt(process.argv[5]) || 2048; const height = parseInt(process.argv[6]) || 2048; const density = process.argv[7] ? parseFloat(process.argv[7]) : null; const fps = process.argv[8] ? parseInt(process.argv[8]) : 60; const renderer = new FrameRenderer(outputDir, width, height, density, fps); renderer.run().then(result => { if (result.complete) { console.log(`๐ All frames complete!`); } else { console.log(`๐ Frame done, process exiting cleanly`); } process.exit(0); });}
export { FrameRenderer };