From af27748a33e83e14d834eab70639da5f2f10acb0 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 23:07:34 -0400 Subject: [PATCH] feat: re-implement dual-brain panel in 3D world (#481) Adds the dual-brain holographic panel back into app.js as a self-contained createDualBrainPanel() function: - Canvas-rendered scorecard sprite at position (10, 3, -8) - Cloud and Local brain orbs with gentle float animation - Scan-line overlay animated each frame via CanvasTexture - Point lights for each brain orb and the main panel Fixes #481 Co-Authored-By: Claude Sonnet 4.6 --- app.js | 208 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) diff --git a/app.js b/app.js index 9a03e40..ac68777 100644 --- a/app.js +++ b/app.js @@ -39,6 +39,8 @@ let thoughtStreamMesh; let harnessPulseMesh; let powerMeterBars = []; let particles, dustParticles; +let dualBrainGroup, dualBrainScanCtx, dualBrainScanTexture; +let cloudOrb, localOrb, cloudOrbLight, localOrbLight, dualBrainLight; let debugOverlay; let frameCount = 0, lastFPSTime = 0, fps = 0; let chatOpen = true; @@ -124,6 +126,7 @@ async function init() { createThoughtStream(); createHarnessPulse(); createSessionPowerMeter(); + createDualBrainPanel(); updateLoad(90); composer = new EffectComposer(renderer); @@ -677,6 +680,183 @@ function createSessionPowerMeter() { scene.add(group); } +// ═══ DUAL-BRAIN PANEL ═══ +function createDualBrainTexture() { + const W = 512, H = 512; + const canvas = document.createElement('canvas'); + canvas.width = W; + canvas.height = H; + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'rgba(0, 6, 20, 0.90)'; + ctx.fillRect(0, 0, W, H); + + ctx.strokeStyle = '#4488ff'; + ctx.lineWidth = 2; + ctx.strokeRect(1, 1, W - 2, H - 2); + + ctx.strokeStyle = '#223366'; + ctx.lineWidth = 1; + ctx.strokeRect(5, 5, W - 10, H - 10); + + ctx.font = 'bold 22px "Courier New", monospace'; + ctx.fillStyle = '#88ccff'; + ctx.textAlign = 'center'; + ctx.fillText('\u25C8 DUAL-BRAIN STATUS', W / 2, 40); + + ctx.strokeStyle = '#1a3a6a'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(20, 52); + ctx.lineTo(W - 20, 52); + ctx.stroke(); + + ctx.font = '11px "Courier New", monospace'; + ctx.fillStyle = '#556688'; + ctx.textAlign = 'left'; + ctx.fillText('BRAIN GAP SCORECARD', 20, 74); + + const categories = [ + { name: 'Triage' }, + { name: 'Tool Use' }, + { name: 'Code Gen' }, + { name: 'Planning' }, + { name: 'Communication' }, + { name: 'Reasoning' }, + ]; + + const barX = 20; + const barW = W - 130; + const barH = 20; + let y = 90; + + for (const cat of categories) { + ctx.font = '13px "Courier New", monospace'; + ctx.fillStyle = '#445566'; + ctx.textAlign = 'left'; + ctx.fillText(cat.name, barX, y + 14); + + ctx.font = 'bold 13px "Courier New", monospace'; + ctx.fillStyle = '#334466'; + ctx.textAlign = 'right'; + ctx.fillText('\u2014', W - 20, y + 14); + + y += 22; + + ctx.fillStyle = 'rgba(255, 255, 255, 0.06)'; + ctx.fillRect(barX, y, barW, barH); + + y += barH + 12; + } + + ctx.strokeStyle = '#1a3a6a'; + ctx.beginPath(); + ctx.moveTo(20, y + 4); + ctx.lineTo(W - 20, y + 4); + ctx.stroke(); + + y += 22; + + ctx.font = 'bold 18px "Courier New", monospace'; + ctx.fillStyle = '#334466'; + ctx.textAlign = 'center'; + ctx.fillText('AWAITING DEPLOYMENT', W / 2, y + 10); + + ctx.font = '11px "Courier New", monospace'; + ctx.fillStyle = '#223344'; + ctx.fillText('Dual-brain system not yet connected', W / 2, y + 32); + + y += 52; + ctx.beginPath(); + ctx.arc(W / 2 - 60, y + 8, 6, 0, Math.PI * 2); + ctx.fillStyle = '#334466'; + ctx.fill(); + ctx.font = '11px "Courier New", monospace'; + ctx.fillStyle = '#334466'; + ctx.textAlign = 'left'; + ctx.fillText('CLOUD', W / 2 - 48, y + 12); + + ctx.beginPath(); + ctx.arc(W / 2 + 30, y + 8, 6, 0, Math.PI * 2); + ctx.fillStyle = '#334466'; + ctx.fill(); + ctx.fillStyle = '#334466'; + ctx.fillText('LOCAL', W / 2 + 42, y + 12); + + return new THREE.CanvasTexture(canvas); +} + +function createDualBrainPanel() { + dualBrainGroup = new THREE.Group(); + dualBrainGroup.position.set(10, 3, -8); + dualBrainGroup.lookAt(0, 3, 0); + scene.add(dualBrainGroup); + + // Main panel sprite + const panelTexture = createDualBrainTexture(); + const panelMat = new THREE.SpriteMaterial({ + map: panelTexture, transparent: true, opacity: 0.92, depthWrite: false, + }); + const panelSprite = new THREE.Sprite(panelMat); + panelSprite.scale.set(5.0, 5.0, 1); + panelSprite.position.set(0, 0, 0); + panelSprite.userData = { baseY: 0, floatPhase: 0, floatSpeed: 0.22, zoomLabel: 'Dual-Brain Status' }; + dualBrainGroup.add(panelSprite); + + // Panel glow light + dualBrainLight = new THREE.PointLight(0x4488ff, 0.6, 10); + dualBrainLight.position.set(0, 0.5, 1); + dualBrainGroup.add(dualBrainLight); + + // Cloud Brain Orb + const cloudOrbGeo = new THREE.SphereGeometry(0.35, 32, 32); + const cloudOrbMat = new THREE.MeshStandardMaterial({ + color: 0x334466, + emissive: new THREE.Color(0x334466), + emissiveIntensity: 0.1, metalness: 0.3, roughness: 0.2, + transparent: true, opacity: 0.85, + }); + cloudOrb = new THREE.Mesh(cloudOrbGeo, cloudOrbMat); + cloudOrb.position.set(-2.0, 3.0, 0); + cloudOrb.userData.zoomLabel = 'Cloud Brain'; + dualBrainGroup.add(cloudOrb); + + cloudOrbLight = new THREE.PointLight(0x334466, 0.15, 5); + cloudOrbLight.position.copy(cloudOrb.position); + dualBrainGroup.add(cloudOrbLight); + + // Local Brain Orb + const localOrbGeo = new THREE.SphereGeometry(0.35, 32, 32); + const localOrbMat = new THREE.MeshStandardMaterial({ + color: 0x334466, + emissive: new THREE.Color(0x334466), + emissiveIntensity: 0.1, metalness: 0.3, roughness: 0.2, + transparent: true, opacity: 0.85, + }); + localOrb = new THREE.Mesh(localOrbGeo, localOrbMat); + localOrb.position.set(2.0, 3.0, 0); + localOrb.userData.zoomLabel = 'Local Brain'; + dualBrainGroup.add(localOrb); + + localOrbLight = new THREE.PointLight(0x334466, 0.15, 5); + localOrbLight.position.copy(localOrb.position); + dualBrainGroup.add(localOrbLight); + + // Scan line overlay + const scanCanvas = document.createElement('canvas'); + scanCanvas.width = 512; + scanCanvas.height = 512; + dualBrainScanCtx = scanCanvas.getContext('2d'); + dualBrainScanTexture = new THREE.CanvasTexture(scanCanvas); + const scanMat = new THREE.SpriteMaterial({ + map: dualBrainScanTexture, transparent: true, opacity: 0.18, depthWrite: false, + }); + const scanSprite = new THREE.Sprite(scanMat); + scanSprite.scale.set(5.0, 5.0, 1); + scanSprite.position.set(0, 0, 0.01); + dualBrainGroup.add(scanSprite); +} + // ═══ VISION SYSTEM ═══ function createVisionPoints(data) { data.forEach(config => { @@ -1442,6 +1622,34 @@ function gameLoop() { // Animate Agents updateAgents(elapsed, delta); + // Animate Dual-Brain Panel + if (dualBrainGroup) { + dualBrainGroup.position.y = 3 + Math.sin(elapsed * 0.22) * 0.15; + if (cloudOrb) { + cloudOrb.position.y = 3 + Math.sin(elapsed * 1.3) * 0.15; + cloudOrb.rotation.y = elapsed * 0.4; + } + if (localOrb) { + localOrb.position.y = 3 + Math.sin(elapsed * 1.3 + Math.PI) * 0.15; + localOrb.rotation.y = -elapsed * 0.4; + } + if (dualBrainLight) { + dualBrainLight.intensity = 0.4 + Math.sin(elapsed * 1.5) * 0.2; + } + if (dualBrainScanCtx && dualBrainScanTexture) { + const W = 512, H = 512; + dualBrainScanCtx.clearRect(0, 0, W, H); + const scanY = ((elapsed * 80) % H); + const grad = dualBrainScanCtx.createLinearGradient(0, scanY - 20, 0, scanY + 20); + grad.addColorStop(0, 'rgba(68, 136, 255, 0)'); + grad.addColorStop(0.5, 'rgba(68, 136, 255, 0.6)'); + grad.addColorStop(1, 'rgba(68, 136, 255, 0)'); + dualBrainScanCtx.fillStyle = grad; + dualBrainScanCtx.fillRect(0, scanY - 20, W, 40); + dualBrainScanTexture.needsUpdate = true; + } + } + // Animate Power Meter powerMeterBars.forEach((bar, i) => { const level = (Math.sin(elapsed * 2 + i * 0.5) * 0.5 + 0.5); -- 2.43.0