# KidLisp Console Screenshots **Status: ✅ IMPLEMENTED** ## Overview Enable KidLisp pieces to output visual screenshots/thumbnails directly to the kidlisp.com console, creating a scrollable visual history of execution frames. ## Existing Implementation (PACK Mode) ### Location [bios.mjs](../system/public/aesthetic.computer/bios.mjs#L15807) ### Current Behavior Every 500 frames in PACK mode, captures and logs a thumbnail to the browser console: ```javascript // 📸 PACK mode: Log frame to console every 500 frames if (window.acPACK_MODE && frameCount % 500n === 0n && canvas) { // ... capture logic console.log( `%c📸 ${pieceCode} %c@ ${ts} %c• Frame ${frameCount} %c[${w}×${h}]`, // ... styled console output ); console.log( `%c `, `font-size: 1px; padding: ${displayH/2}px; background: url("${dataUrl}") ...` ); } ``` ### Key Details - Uses canvas `toDataURL('image/png')` for capture - Scales to 3x for crisp pixelated display - Display size constrained to ~200px max dimension - Styled with CSS colors for piece code, timestamp, frame count, dimensions ## KidLisp Console Protocol ### Message Format [kidlisp.mjs](../system/public/aesthetic.computer/lib/kidlisp.mjs#L327) ```javascript function postKidlispConsole(level, message, meta) { const payload = { type: "kidlisp-console", level, message }; if (meta) { if (meta.loc) payload.loc = meta.loc; if (meta.kind) payload.kind = meta.kind; if (meta.embeddedSource) payload.embeddedSource = meta.embeddedSource; } postToParent(payload); } ``` ### Console Receiver [kidlisp.com/index.html](../system/public/kidlisp.com/index.html#L6568) ```javascript } else if (event.data && event.data.type === 'kidlisp-console') { addConsoleEntry(event.data.message, event.data.level, event.data.loc, { embeddedSource: event.data.embeddedSource, embeddedSourceCode: event.data.embeddedSourceCode }); } ``` ## Implementation Plan ### Phase 1: Extend Console Protocol for Images #### 1.1 Add New Message Type Extend `postKidlispConsole` to support image data: ```javascript // kidlisp.mjs function postKidlispConsoleImage(imageDataUrl, meta = {}) { if (!isKidlispConsoleEnabled()) return; const payload = { type: "kidlisp-console-image", imageDataUrl, frameCount: meta.frameCount, timestamp: meta.timestamp || Date.now(), dimensions: meta.dimensions, pieceCode: meta.pieceCode, embeddedSource: meta.embeddedSource }; postToParent(payload); } ``` #### 1.2 Export Function Add to KidLisp exports so it can be called from bios.mjs and kidlisp pieces. ### Phase 2: Create Reusable Frame Capture Utility #### 2.1 New Module: `frame-capture.mjs` Location: `system/public/aesthetic.computer/lib/frame-capture.mjs` ```javascript /** * Capture a frame from a canvas and generate a data URL * @param {HTMLCanvasElement} canvas - Source canvas * @param {Object} options - Capture options * @returns {Object} - { dataUrl, displayWidth, displayHeight, dimensions } */ export function captureFrame(canvas, options = {}) { const { scaleFactor = 3, // For crisp pixelated display displayMax = 200, // Max display dimension format = 'image/png', // Output format quality = 1.0 // Quality for jpeg/webp } = options; const w = canvas.width; const h = canvas.height; // Create scaled thumbnail const thumbW = w * scaleFactor; const thumbH = h * scaleFactor; const thumbCanvas = document.createElement('canvas'); thumbCanvas.width = thumbW; thumbCanvas.height = thumbH; const thumbCtx = thumbCanvas.getContext('2d'); thumbCtx.imageSmoothingEnabled = false; thumbCtx.drawImage(canvas, 0, 0, thumbW, thumbH); const dataUrl = thumbCanvas.toDataURL(format, quality); // Calculate display dimensions const aspect = w / h; const displayWidth = aspect >= 1 ? displayMax : Math.round(displayMax * aspect); const displayHeight = aspect >= 1 ? Math.round(displayMax / aspect) : displayMax; return { dataUrl, displayWidth, displayHeight, dimensions: { width: w, height: h } }; } /** * Format timestamp for display */ export function formatTimestamp(date = new Date()) { return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true }); } ``` #### 2.2 Update bios.mjs Refactor to use the shared module: ```javascript import { captureFrame, formatTimestamp } from './lib/frame-capture.mjs'; // In render loop: if (window.acPACK_MODE && frameCount % 500n === 0n && canvas) { try { const { dataUrl, displayWidth, displayHeight, dimensions } = captureFrame(canvas); const ts = formatTimestamp(); const pieceCode = window.acPACK_PIECE || 'piece'; // Log to browser console (existing behavior) console.log(/* ... existing styled output ... */); // Also send to kidlisp.com console if enabled if (isKidlispConsoleEnabled()) { postKidlispConsoleImage(dataUrl, { frameCount: Number(frameCount), timestamp: ts, pieceCode, dimensions }); } } catch (e) { /* ... */ } } ``` ### Phase 3: Console Image Display #### 3.1 Add CSS Styles In kidlisp.com/index.html: ```css .console-image-entry { display: flex; align-items: flex-start; gap: 12px; padding: 8px 12px; border-radius: 6px; background: rgba(78, 205, 196, 0.1); border-left: 3px solid #4ecdc4; margin: 4px 0; } .console-image-thumbnail { image-rendering: pixelated; border-radius: 4px; cursor: pointer; transition: transform 0.2s; } .console-image-thumbnail:hover { transform: scale(1.5); z-index: 100; } .console-image-actions { display: flex; gap: 6px; margin-top: 6px; } .console-image-download { background: #4ecdc4; color: #1e1e2e; border: none; border-radius: 4px; padding: 4px 8px; font-size: 11px; cursor: pointer; font-weight: bold; } .console-image-download:hover { background: #3dbdb5; } .console-image-meta { font-size: 11px; font-family: monospace; color: #888; } .console-image-meta .piece-code { color: #4ecdc4; font-weight: bold; } .console-image-meta .timestamp { color: #f8b500; } .console-image-meta .frame-count { color: #666; } ``` #### 3.2 Add Message Handler ```javascript } else if (event.data && event.data.type === 'kidlisp-console-image') { addConsoleImageEntry(event.data); } ``` #### 3.3 Create Image Entry Function ```javascript function addConsoleImageEntry(data) { const { imageDataUrl, frameCount, timestamp, dimensions, pieceCode } = data; const entry = document.createElement('div'); entry.className = 'console-entry console-image-entry'; // Thumbnail image const img = document.createElement('img'); img.src = imageDataUrl; img.className = 'console-image-thumbnail'; img.style.width = '120px'; // Fixed preview width img.style.height = 'auto'; img.title = 'Click to view full size'; img.addEventListener('click', () => showImagePopup(imageDataUrl, dimensions, pieceCode, frameCount)); // Metadata const meta = document.createElement('div'); meta.className = 'console-image-meta'; meta.innerHTML = ` 📸 ${pieceCode || '$piece'}
@ ${timestamp}
Frame ${frameCount}
[${dimensions?.width}×${dimensions?.height}] `; // Actions (download button) const actions = document.createElement('div'); actions.className = 'console-image-actions'; const downloadBtn = document.createElement('button'); downloadBtn.className = 'console-image-download'; downloadBtn.textContent = '⬇ Download'; downloadBtn.addEventListener('click', () => downloadImage(imageDataUrl, pieceCode, frameCount)); actions.appendChild(downloadBtn); meta.appendChild(actions); entry.appendChild(img); entry.appendChild(meta); consoleOutput.appendChild(entry); consoleOutput.scrollTop = consoleOutput.scrollHeight; } function downloadImage(dataUrl, pieceCode, frameCount) { const filename = `${pieceCode || 'piece'}-frame${frameCount || 0}-${Date.now()}.png`; const link = document.createElement('a'); link.href = dataUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function showImagePopup(dataUrl, dimensions, pieceCode, frameCount) { // Similar to showEmbeddedSourcePopup but for images const existing = document.getElementById('console-image-popup'); if (existing) existing.remove(); const popup = document.createElement('div'); popup.id = 'console-image-popup'; popup.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1e1e2e; border: 2px solid #4ecdc4; border-radius: 8px; padding: 16px; z-index: 10000; box-shadow: 0 4px 20px rgba(0,0,0,0.5); `; const img = document.createElement('img'); img.src = dataUrl; img.style.cssText = 'image-rendering: pixelated; max-width: 80vw; max-height: 70vh; display: block;'; // Download button in popup const downloadBtn = document.createElement('button'); downloadBtn.textContent = '⬇ Download PNG'; downloadBtn.style.cssText = ` display: block; width: 100%; margin-top: 12px; padding: 10px; background: #4ecdc4; color: #1e1e2e; border: none; border-radius: 6px; font-size: 14px; font-weight: bold; cursor: pointer; `; downloadBtn.onclick = () => downloadImage(dataUrl, pieceCode, frameCount); const closeBtn = document.createElement('button'); closeBtn.textContent = '✕'; closeBtn.style.cssText = 'position: absolute; top: 8px; right: 8px; background: none; border: none; color: #888; cursor: pointer; font-size: 18px;'; closeBtn.onclick = () => popup.remove(); popup.appendChild(closeBtn); popup.appendChild(img); popup.appendChild(downloadBtn); document.body.appendChild(popup); // Close on escape const closeHandler = (e) => { if (e.key === 'Escape') { popup.remove(); document.removeEventListener('keydown', closeHandler); } }; document.addEventListener('keydown', closeHandler); } ``` ### Phase 4: KidLisp `snap` Function #### 4.1 Add `snap` to KidLisp API Allow KidLisp pieces to explicitly capture and output screenshots: ```javascript // In globalEnvCache snap: (api, args = []) => { // Capture current frame to console if (api.screen?.canvas) { const { dataUrl, displayWidth, displayHeight, dimensions } = captureFrame(api.screen.canvas); // Log to browser console console.log( `%c📸 snap`, `color: #4ecdc4; font-weight: bold;` ); console.log( `%c `, `font-size: 1px; padding: ${displayHeight/2}px ${displayWidth/2}px; background: url("${dataUrl}") no-repeat center; background-size: ${displayWidth}px ${displayHeight}px; image-rendering: pixelated;` ); // Send to kidlisp.com console if (isKidlispConsoleEnabled()) { postKidlispConsoleImage(dataUrl, { frameCount: api.frameCount || 0, timestamp: formatTimestamp(), pieceCode: api.slug || 'piece', dimensions }); } } } ``` #### 4.2 Usage Example ```lisp ; Take a snapshot every second (1s (snap)) ; Or on tap (tap (snap)) ``` ### Phase 5: Auto-Screenshot on Errors Automatically capture the frame state when validation/runtime errors occur: ```javascript // In parse() error handling if (validationErrors.length > 0 && this.api?.screen?.canvas) { try { const { dataUrl } = captureFrame(this.api.screen.canvas, { displayMax: 150 }); postKidlispConsoleImage(dataUrl, { frameCount: this.api.frameCount, timestamp: formatTimestamp(), pieceCode: this.embeddedSourceId || this.api.slug, isErrorSnapshot: true }); } catch (e) { /* fail silently */ } } ``` ## File Changes Summary | File | Changes | |------|---------| | `lib/frame-capture.mjs` | **NEW** - Shared frame capture utility | | `lib/kidlisp.mjs` | Add `postKidlispConsoleImage`, `snap` function, error snapshots | | `bios.mjs` | Refactor to use shared module, add console image posting | | `kidlisp.com/index.html` | Add CSS, message handler, image display functions | ## Migration Path 1. Create `frame-capture.mjs` with shared utilities 2. Update `kidlisp.mjs` with new protocol and `snap` function 3. Update `kidlisp.com/index.html` with image display 4. Refactor `bios.mjs` to use shared code 5. Test in both PACK mode and kidlisp.com editor ## Future Enhancements - **GIF Animation**: Capture multiple frames as animated GIF for loops - **Image Gallery View**: Toggle between list/grid view of captured images - **Comparison Mode**: Side-by-side comparison of snapshots - **Auto-capture on specific events**: Capture on `tap`, `draw`, timing events - **Size optimization**: Use WebP format with quality settings for smaller payloads - **Batch Download**: Download all captured frames as a ZIP archive