diff --git a/ac-electron/main.js b/ac-electron/main.js index 463911fea4..0b900e05a8 100644 --- a/ac-electron/main.js +++ b/ac-electron/main.js @@ -801,6 +801,90 @@ function rebuildTrayMenu() { menuItems.push({ type: 'separator' }); + // FF1 Art Computer Bridge section + const ff1Devices = ff1Bridge.getDevices(); + const ff1Running = ff1Bridge.isRunning(); + menuItems.push({ + label: '🖼️ FF1 Art Computer', + submenu: [ + { + label: ff1Running ? '✓ Bridge Running (port 19999)' : '✗ Bridge Not Running', + enabled: false + }, + { + label: `Devices Found: ${ff1Devices.length}`, + enabled: false + }, + { type: 'separator' }, + ...ff1Devices.map(device => ({ + label: `${device.deviceId} (${device.ip})`, + submenu: [ + { + label: 'Cast Current Piece', + click: () => { + const win = getFocusedWindow(); + if (win) { + const mode = getFocusedWindowMode(); + const baseUrl = mode === 'production' ? 'https://aesthetic.computer' : 'http://localhost:8888'; + const piece = currentPiece || 'prompt'; + const url = `${baseUrl}/${piece}`; + // Send cast request through the bridge + const http = require('http'); + const payload = JSON.stringify({ + deviceId: device.deviceId, + playlist: [{ url, duration: 0 }] + }); + const req = http.request({ + hostname: '127.0.0.1', + port: 19999, + path: '/cast', + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + req.write(payload); + req.end(); + } + } + }, + { + label: 'Open Device IP', + click: () => shell.openExternal(`http://${device.ip}:1111`) + } + ] + })), + ...(ff1Devices.length === 0 ? [{ + label: 'No devices found', + enabled: false + }] : []), + { type: 'separator' }, + { + label: 'Scan for Devices', + click: () => { + ff1Bridge.scanForDevices(); + setTimeout(() => rebuildTrayMenu(), 3000); + } + }, + { + label: 'Open KidLisp.com Editor', + click: () => shell.openExternal('https://kidlisp.com') + }, + { type: 'separator' }, + { + label: 'About FF1 Bridge', + click: () => { + dialog.showMessageBox({ + type: 'info', + title: 'FF1 Art Computer Bridge', + message: 'FF1 Bridge', + detail: 'The FF1 Bridge allows kidlisp.com to send KidLisp pieces to FF1 Art Computers on your local network.\n\nThe bridge runs on port 19999 and automatically discovers FF1 devices via mDNS.\n\nVisit kidlisp.com to create and cast pieces!' + }); + } + } + ] + }); + + menuItems.push({ type: 'separator' }); + // Edit section menuItems.push({ label: 'Edit', @@ -1007,8 +1091,8 @@ function navigateToPiece(piece) { } // Open a new AC Pane window -async function openAcPaneWindow() { - return openAcPaneWindowInternal(); +async function openAcPaneWindow(options = {}) { + return openAcPaneWindowInternal(options); } // Open KidLisp window (kidlisp.com) @@ -1048,14 +1132,52 @@ let ptyProcessFor3D = null; let lastKnownCols = 120; let lastKnownRows = 40; -async function openAcPaneWindowInternal() { +// Calculate offset position for new windows to avoid overlap +function getOffsetWindowPosition(sourceWindow, index = 0) { + const offset = 40; // Pixels to offset each new window + const { width, height } = screen.getPrimaryDisplay().workAreaSize; + + let baseX, baseY; + + if (sourceWindow && !sourceWindow.isDestroyed()) { + const [srcX, srcY] = sourceWindow.getPosition(); + const [srcW, srcH] = sourceWindow.getSize(); + // Position to the right of source window + baseX = srcX + srcW + 20; + baseY = srcY; + // If it would go off screen, wrap to upper-left with offset + if (baseX + 680 > width) { + baseX = 50 + (index * offset); + baseY = 50 + (index * offset); + } + } else { + // No source window, use cascading from top-left + baseX = 100 + (index * offset); + baseY = 100 + (index * offset); + } + + // Apply index offset for multiple windows + const x = Math.min(baseX + (index * offset), width - 680); + const y = Math.min(baseY + (index * offset), height - 520); + + return { x: Math.max(0, x), y: Math.max(0, y) }; +} + +async function openAcPaneWindowInternal(options = {}) { + const { sourceWindow = null, index = 0 } = options; + // Start with a wide window - extra height for mode tags at bottom const winWidth = 680; const winHeight = 520; + // Calculate position to avoid overlap + const { x, y } = getOffsetWindowPosition(sourceWindow, index); + const win = new BrowserWindow({ width: winWidth, height: winHeight, + x, + y, minWidth: 320, minHeight: 240, title: 'AC Pane', @@ -1880,9 +2002,10 @@ ipcMain.on('move-window', (event, position) => { }); // Open/close windows from inside the embedded AC prompt (via webview popup interception) -ipcMain.handle('ac-open-window', async (event, { url } = {}) => { - console.log('[main] ac-open-window called with url:', url); - const { window: newWindow } = await openAcPaneWindow(); +ipcMain.handle('ac-open-window', async (event, { url, index = 0, total = 1 } = {}) => { + console.log('[main] ac-open-window called with url:', url, 'index:', index, 'total:', total); + const sourceWindow = BrowserWindow.fromWebContents(event.sender); + const { window: newWindow } = await openAcPaneWindowInternal({ sourceWindow, index }); console.log('[main] openAcPaneWindow returned:', !!newWindow); if (url && newWindow) { newWindow.webContents.once('did-finish-load', () => { @@ -2156,7 +2279,11 @@ app.on('web-contents-created', (event, contents) => { // Handle new window request (from prompt.mjs '+' command) // Open a new AC Pane and navigate to the URL - openAcPaneWindow().then(({ window: newWin }) => { + // Find the source window from the webview's host + const sourceWindow = BrowserWindow.getAllWindows().find(win => + !win.isDestroyed() && win.webContents.id === contents.hostWebContents?.id + ); + openAcPaneWindowInternal({ sourceWindow, index: 0 }).then(({ window: newWin }) => { if (newWin && !newWin.isDestroyed()) { newWin.webContents.once('did-finish-load', () => { if (!newWin.isDestroyed()) { diff --git a/ac-electron/package-lock.json b/ac-electron/package-lock.json index 42bb6b824c..bd33f679e4 100644 --- a/ac-electron/package-lock.json +++ b/ac-electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "aesthetic-computer", - "version": "0.1.9", + "version": "0.1.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aesthetic-computer", - "version": "0.1.9", + "version": "0.1.31", "license": "MIT", "dependencies": { "@electron/rebuild": "^4.0.2", diff --git a/ac-electron/package.json b/ac-electron/package.json index 4fb6da5eb2..c631515243 100644 --- a/ac-electron/package.json +++ b/ac-electron/package.json @@ -1,6 +1,6 @@ { "name": "aesthetic-computer", - "version": "0.1.30", + "version": "0.1.31", "description": "Aesthetic Computer", "homepage": "https://aesthetic.computer", "author": "Aesthetic Computer ", diff --git a/ac-electron/renderer/flip-view.html b/ac-electron/renderer/flip-view.html index adb1ae9b35..0d7c0d1bd9 100644 --- a/ac-electron/renderer/flip-view.html +++ b/ac-electron/renderer/flip-view.html @@ -887,7 +887,19 @@ } console.log('[flip] Opening new window with url:', url); - ipcRenderer.invoke('ac-open-window', { url }); + ipcRenderer.invoke('ac-open-window', { url, index: 0 }); + }); + + // Listen for postMessage from webview (bios.mjs sends ac-open-window messages) + window.addEventListener('message', (e) => { + if (e.data?.type === 'ac-open-window') { + console.log('[flip] Received ac-open-window postMessage:', e.data); + ipcRenderer.invoke('ac-open-window', { + url: e.data.url, + index: e.data.index || 0, + total: e.data.total || 1 + }); + } }); // Also try did-create-window (Electron 22+) diff --git a/reports/sandbox-canvas-performance-report.md b/reports/sandbox-canvas-performance-report.md new file mode 100644 index 0000000000..f240c88cd1 --- /dev/null +++ b/reports/sandbox-canvas-performance-report.md @@ -0,0 +1,205 @@ +# Iframe Sandbox Canvas Performance Impact Report + +## Executive Summary + +When a web page runs inside an ` +``` + +This is **missing `allow-same-origin`**, which causes: + +1. Content treated as opaque/cross-origin to itself +2. Canvas security checks activate on every pixel operation +3. Browser may fall back to software rendering +4. ~50% FPS drop observed + +**Solution**: Users must enable "Advanced Mode" on objkt.com, which adds `allow-same-origin`. + +--- + +## Affected KidLisp Features + +The `$roz` code uses these operations that suffer in sandboxed contexts: + +| Feature | KidLisp Command | Canvas Operation | Impact | +|---------|-----------------|------------------|--------| +| Render | (every frame) | `putImageData` | ⚠️ 50% slower | +| Scroll | `scroll` | Reads/writes pixels | ⚠️ Slower | +| Contrast | `contrast` | Pixel manipulation | ⚠️ Slower | +| Color parsing | `ink`, `fade` | `getImageData` (fallback) | ⚠️ Potential slowdown | + +--- + +## Implemented Optimizations + +### ✅ Sandbox Detection (bios.mjs) + +Added early detection of opaque origin (sandboxed iframe without `allow-same-origin`): + +```javascript +// Detects objkt.com's sandbox="allow-scripts allow-downloads" +const isOpaqueOrigin = (() => { + try { + if (window.origin === 'null') return true; + localStorage.getItem('__sandbox_test__'); + return false; + } catch (e) { + return true; + } +})(); + +// Exposed globally for pieces to detect +globalThis.acIsSandboxed = isOpaqueOrigin; +``` + +### ✅ Canvas Context Hints (bios.mjs) + +Added `willReadFrequently: true` hint to all canvas contexts when in sandboxed mode: + +| Canvas | Line | Purpose | +|--------|------|---------| +| `ctx` (main) | ~936 | Primary render target - **already had hint** | +| `uiCtx` | ~1025 | UI overlay | +| `debugCtx` | ~1029 | Debug overlay | +| `ffCtx` | ~1036 | Freeze frame buffer | +| `octx` | ~1041 | Corner label overlay | +| `dirtyBoxCtx` | ~1045 | Dirty box updates | +| `glazeCompositeCtx` | ~1061 | Glaze compositing | +| `coerceToRGB.ctx` | ~1306 | Color parsing - **already had hint** | + +This tells the browser to use software rendering from the start, avoiding the penalty of constant GPU→CPU security checks. + +### ✅ Export Guards (bufferToBlob, captureFrame) + +Added early-return guards to functions that call `toBlob`/`toDataURL`: + +- **`bufferToBlob()`** in bios.mjs - Returns `null` with warning in sandbox +- **`captureFrame()`** in frame-capture.mjs - Returns `null` in sandbox + +--- + +## Recommendations + +### For objkt.com / NFT Platforms + +1. **Add `allow-same-origin`** to sandbox attribute for art pieces +2. Or provide a performance toggle (like objkt's "Advanced Mode") + +### For KidLisp Bundles + +1. ✅ **Already done**: Disabled `acPACK_MODE` (screenshot features) +2. ✅ **Already done**: Disabled console auto-snaps +3. ✅ **Implemented**: Detect sandboxed context and log warning +4. **Consider**: Use `OffscreenCanvas` for heavy operations (when supported) + +### For Aesthetic Computer Runtime + +1. ✅ **Implemented**: Detect opaque origin early +2. ✅ **Implemented**: Skip impossible operations (`toBlob`, `toDataURL`) +3. ✅ **Implemented**: Add `willReadFrequently` hint to canvas contexts +4. **Consider**: Add FPS counter warning in sandbox mode + +--- + +## Files Changed + +| File | Changes | +|------|---------| +| [bios.mjs](../system/public/aesthetic.computer/bios.mjs) | Added `isOpaqueOrigin` detection, `willReadFrequently` hints, `bufferToBlob` guard | +| [frame-capture.mjs](../system/public/aesthetic.computer/lib/frame-capture.mjs) | Added `isOpaqueOrigin` guard to `captureFrame()` | + +--- + +## References + +- [WHATWG HTML Spec - Canvas Security](https://html.spec.whatwg.org/multipage/canvas.html#security-with-canvas-elements) +- [MDN - getImageData() Security](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData) +- [MDN - iframe sandbox attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox) +- [MDN - Same-origin policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) + +--- + +*Report generated: January 27, 2026* +*Context: FF1 display device performance investigation for objkt.com tokens* +*Updated: Added implementation details for sandbox detection and canvas optimizations* diff --git a/system/netlify/functions/bundle-html.js b/system/netlify/functions/bundle-html.js index 9b3edf4633..9cfba443d5 100644 --- a/system/netlify/functions/bundle-html.js +++ b/system/netlify/functions/bundle-html.js @@ -637,7 +637,9 @@ function generateJSPieceHTMLBundle(opts) {