/** * CAPT Memory Palace - Main Entry Point * * Initializes Three.js scene and handles user interactions. * See DEPLOY.md for API integration details. */ // Scene globals let scene, camera, renderer; let palaceRooms = []; let connections = []; let animationId = null; let raycaster, mouse; let selectedRoom = null; // Particle system for neural connections let particleSystem = null; let particleGeometry = null; let particleMaterial = null; const particles = []; function initParticles() { particleGeometry = new THREE.BufferGeometry(); const particleCount = 500; const positions = new Float32Array(particleCount * 3); const colors = new Float32Array(particleCount * 3); for (let i = 0; i < particleCount; i++) { positions[i * 3] = (Math.random() - 0.5) * 30; positions[i * 3 + 1] = (Math.random() - 0.5) * 20; positions[i * 3 + 2] = (Math.random() - 0.5) * 30; colors[i * 3] = 0.5 + Math.random() * 0.5; colors[i * 3 + 1] = 0.3; colors[i * 3 + 2] = 0.8; particles.push({ velocity: new THREE.Vector3( (Math.random() - 0.5) * 0.02, (Math.random() - 0.5) * 0.02, (Math.random() - 0.5) * 0.02 ), baseColor: new THREE.Color(0.5, 0.3, 0.8) }); } particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); particleGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); particleMaterial = new THREE.PointsMaterial({ size: 0.08, vertexColors: true, transparent: true, opacity: 0.6, blending: THREE.AdditiveBlending }); particleSystem = new THREE.Points(particleGeometry, particleMaterial); scene.add(particleSystem); } function updateParticles() { if (!particleSystem) return; const positions = particleGeometry.attributes.position.array; const colors = particleGeometry.attributes.color.array; for (let i = 0; i < particles.length; i++) { const p = particles[i]; positions[i * 3] += p.velocity.x; positions[i * 3 + 1] += p.velocity.y; positions[i * 3 + 2] += p.velocity.z; // Wrap around if (Math.abs(positions[i * 3]) > 15) p.velocity.x *= -1; if (Math.abs(positions[i * 3 + 1]) > 10) p.velocity.y *= -1; if (Math.abs(positions[i * 3 + 2]) > 15) p.velocity.z *= -1; // Pulse color based on activity const activity = window.captActivity || 0; const pulse = Math.sin(Date.now() * 0.002 + i * 0.1) * 0.3 + 0.7; colors[i * 3] = (0.5 + activity * 0.5) * pulse; colors[i * 3 + 1] = 0.3 * pulse; colors[i * 3 + 2] = (0.8 + activity * 0.2) * pulse; } particleGeometry.attributes.position.needsUpdate = true; particleGeometry.attributes.color.needsUpdate = true; } // Configuration const CONFIG = { theme: 'cinematic', cameraFOV: 60, cameraNear: 0.1, cameraFar: 1000 }; /** * Initialize the 3D scene */ function initScene(containerId = 'canvas-container') { const container = document.getElementById(containerId); if (!container) { console.error('Container not found:', containerId); return; } // Scene scene = new THREE.Scene(); scene.background = new THREE.Color(0x0a0a12); scene.fog = new THREE.FogExp2(0x0a0a12, 0.02); // Camera camera = new THREE.PerspectiveCamera( CONFIG.cameraFOV, window.innerWidth / window.innerHeight, CONFIG.cameraNear, CONFIG.cameraFar ); camera.position.set(0, 5, 15); // Renderer renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.2; container.appendChild(renderer.domElement); // Raycaster for click detection raycaster = new THREE.Raycaster(); mouse = new THREE.Vector2(); // Lights const ambientLight = new THREE.AmbientLight(0x404040, 0.5); scene.add(ambientLight); const pointLight1 = new THREE.PointLight(0x8b5cf6, 1, 50); pointLight1.position.set(10, 10, 10); scene.add(pointLight1); const pointLight2 = new THREE.PointLight(0xec4899, 0.8, 50); pointLight2.position.set(-10, 5, -10); scene.add(pointLight2); // Grid helper const gridHelper = new THREE.GridHelper(50, 50, 0x1a1025, 0x1a1025); gridHelper.position.y = -2; scene.add(gridHelper); // Handle resize window.addEventListener('resize', onWindowResize); // Mouse events renderer.domElement.addEventListener('click', onMouseClick); renderer.domElement.addEventListener('mousemove', onMouseMove); // Touch events for mobile renderer.domElement.addEventListener('touchstart', onTouchStart, { passive: false }); renderer.domElement.addEventListener('touchmove', onTouchMove, { passive: false }); renderer.domElement.addEventListener('touchend', onTouchEnd); // Keyboard controls document.addEventListener('keydown', onKeyDown); // Voiceover setup let voiceoverPlayed = false; const maybePlayVoiceover = () => { if (voiceoverPlayed) return; voiceoverPlayed = true; // Initialize audio if not already if (window.AudioEngine && !window.AudioEngine.context) { window.AudioEngine.init().then(() => { window.AudioEngine.loadVoiceover().then(() => { window.AudioEngine.playVoiceover(); }); }); } else if (window.AudioEngine) { window.AudioEngine.loadVoiceover().then(() => { window.AudioEngine.playVoiceover(); }); } }; // Play voiceover on first user interaction const firstInteractionHandler = () => { maybePlayVoiceover(); document.removeEventListener('click', firstInteractionHandler); document.removeEventListener('keydown', firstInteractionHandler); document.removeEventListener('touchstart', firstInteractionHandler); }; document.addEventListener('click', firstInteractionHandler); document.addEventListener('keydown', firstInteractionHandler); document.addEventListener('touchstart', firstInteractionHandler); // Start render loop initParticles(); animate(); return { scene, camera, renderer }; } /** * Touch handling for mobile */ let touchStartX = 0; let touchStartY = 0; let touchStartTime = 0; let isTouchDragging = false; function onTouchStart(event) { if (event.touches.length === 1) { touchStartX = event.touches[0].clientX; touchStartY = event.touches[0].clientY; touchStartTime = Date.now(); isTouchDragging = false; } } function onTouchMove(event) { event.preventDefault(); // Prevent scroll if (event.touches.length === 1) { const deltaX = event.touches[0].clientX - touchStartX; const deltaY = event.touches[0].clientY - touchStartY; // If moved enough, start dragging if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { isTouchDragging = true; // Rotate camera around center camera.position.x += deltaX * 0.02; camera.position.y -= deltaY * 0.02; camera.lookAt(0, 0, 0); touchStartX = event.touches[0].clientX; touchStartY = event.touches[0].clientY; } } else if (event.touches.length === 2) { // Pinch zoom const dist = Math.hypot( event.touches[0].clientX - event.touches[1].clientX, event.touches[0].clientY - event.touches[1].clientY ); if (this.lastPinchDist) { const delta = dist - this.lastPinchDist; camera.position.z += delta * 0.05; camera.position.z = Math.max(5, Math.min(30, camera.position.z)); } this.lastPinchDist = dist; } } function onTouchEnd(event) { const touchDuration = Date.now() - touchStartTime; // Tap to select (quick touch without drag) if (!isTouchDragging && touchDuration < 300) { const touch = event.changedTouches[0]; onMouseClick({ clientX: touch.clientX, clientY: touch.clientY }); } isTouchDragging = false; this.lastPinchDist = null; } /** * Window resize handler */ function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } /** * Mouse click handler - select rooms */ function onMouseClick(event) { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const meshes = palaceRooms.filter(r => r.mesh).map(r => r.mesh); const intersects = raycaster.intersectObjects(meshes); if (intersects.length > 0) { const clickedMesh = intersects[0].object; const room = palaceRooms.find(r => r.mesh === clickedMesh); if (room) { showRoomInfo(room); } } else { hideRoomInfo(); } } /** * Mouse move handler - hover effects */ function onMouseMove(event) { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const meshes = palaceRooms.filter(r => r.mesh).map(r => r.mesh); const intersects = raycaster.intersectObjects(meshes); // Reset all rooms palaceRooms.forEach(r => { if (r.mesh && r.mesh.material) { r.mesh.material.emissiveIntensity = 0.3; } }); // Highlight hovered room if (intersects.length > 0) { const hoveredMesh = intersects[0].object; const room = palaceRooms.find(r => r.mesh === hoveredMesh); if (room && room.mesh.material) { room.mesh.material.emissiveIntensity = 0.8; } renderer.domElement.style.cursor = 'pointer'; } else { renderer.domElement.style.cursor = 'default'; } } /** * Keyboard controls */ const keys = {}; function onKeyDown(event) { keys[event.key.toLowerCase()] = true; // Space - toggle tour if (event.code === 'Space') { event.preventDefault(); Tour.toggle(camera, { x: 0, y: 0, z: 0 }); window.startCinematicTour = () => Tour.toggle(camera, { x: 0, y: 0, z: 0 }); } // R - reset view if (event.key.toLowerCase() === 'r') { camera.position.set(0, 5, 15); camera.lookAt(0, 0, 0); } } document.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; }); /** * Animation loop */ function animate() { updateParticles(); animationId = requestAnimationFrame(animate); // Keyboard camera movement const moveSpeed = 0.15; if (keys['w']) { camera.position.z -= moveSpeed; camera.position.y += moveSpeed * 0.3; } if (keys['s']) { camera.position.z += moveSpeed; camera.position.y -= moveSpeed * 0.3; } if (keys['a']) { camera.position.x -= moveSpeed; } if (keys['d']) { camera.position.x += moveSpeed; } if (keys['q']) { camera.position.y -= moveSpeed; } if (keys['e']) { camera.position.y += moveSpeed; } // Animate rooms (pulse effect) palaceRooms.forEach((room, i) => { if (room.mesh) { const scale = 1 + Math.sin(Date.now() * 0.001 + i) * 0.05; room.mesh.scale.set(scale, scale, scale); } }); // Animate connections (flow effect) connections.forEach((conn, i) => { if (conn.material) { conn.material.dashOffset -= 0.5; } }); renderer.render(scene, camera); } /** * Load palace configuration */ function loadPalace(config) { if (!config.rooms) return; // Clear existing palaceRooms.forEach(r => { if (r.mesh) scene.remove(r.mesh); }); connections.forEach(c => scene.remove(c)); palaceRooms = []; connections = []; // Create rooms config.rooms.forEach((roomData, index) => { let color = roomData.color || 0x8b5cf6; if (typeof color === 'string' && color.startsWith('#')) { color = parseInt(color.slice(1), 16); } const geometry = new THREE.SphereGeometry(1, 32, 32); const material = new THREE.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.3, metalness: 0.8, roughness: 0.2, transparent: true, opacity: 0.9 }); const mesh = new THREE.Mesh(geometry, material); mesh.position.set( roomData.position?.x || Math.cos(index * Math.PI * 2 / config.rooms.length) * 5, roomData.position?.y || 0, roomData.position?.z || Math.sin(index * Math.PI * 2 / config.rooms.length) * 5 ); // Add label sprite const label = createTextSprite(roomData.label); label.position.copy(mesh.position); label.position.y += 1.5; scene.add(label); scene.add(mesh); palaceRooms.push({ ...roomData, mesh, label }); }); // Create connections if (config.connections) { config.connections.forEach(conn => { const fromRoom = palaceRooms.find(r => r.id === conn.from); const toRoom = palaceRooms.find(r => r.id === conn.to); if (fromRoom && toRoom && fromRoom.mesh && toRoom.mesh) { const points = [fromRoom.mesh.position.clone(), toRoom.mesh.position.clone()]; const geometry = new THREE.BufferGeometry().setFromPoints(points); const material = new THREE.LineDashedMaterial({ color: 0x6b5f7a, dashSize: 0.3, gapSize: 0.1 }); const line = new THREE.Line(geometry, material); line.computeLineDistances(); scene.add(line); connections.push(line); } }); } // Update UI stats document.getElementById('stat-modules').textContent = palaceRooms.length; } /** * Returns default palace configuration when palace.json is not available */function getDefaultConfig() { return { "schemaVersion": "3.0.0", "name": "CAPT Cognitive Architecture", "theme": "cinematic", "rooms": [ { "id": "PULSE", "label": "PULSE", "type": "input", "color": "#8b5cf6", "position": { "x": 0, "y": 0, "z": 6 }, "capacity": 100 }, { "id": "NEDA", "label": "NEDA", "type": "processing", "color": "#a855f7", "position": { "x": 3, "y": 1, "z": 3 }, "capacity": 100 }, { "id": "HMC", "label": "HMC", "type": "memory", "color": "#c084fc", "position": { "x": 5, "y": 0.5, "z": 0 }, "capacity": 100 }, { "id": "ECHO", "label": "ECHO", "type": "memory", "color": "#fb7185", "position": { "x": 3, "y": 0.8, "z": -3 }, "capacity": 100 }, { "id": "CIG", "label": "CIG", "type": "processing", "color": "#d946ef", "position": { "x": 0, "y": 1.2, "z": -5 }, "capacity": 100 }, { "id": "HDR", "label": "HDR", "type": "processing", "color": "#e879f9", "position": { "x": -3, "y": 0.6, "z": -3 }, "capacity": 100 }, { "id": "QIPC", "label": "QIPC", "type": "quorum", "color": "#f472b6", "position": { "x": -5, "y": 1.5, "z": 0 }, "capacity": 100 }, { "id": "META", "label": "META", "type": "processing", "color": "#f43f5e", "position": { "x": -3, "y": 0.9, "z": 3 }, "capacity": 100 }, { "id": "IMMU", "label": "IMMU", "type": "validation", "color": "#10b981", "position": { "x": 0, "y": 0.7, "z": 4 }, "capacity": 100 }, { "id": "NDS", "label": "NDS", "type": "output", "color": "#06b6d4", "position": { "x": 0, "y": 2, "z": 0 }, "capacity": 100 } ], "connections": [ { "from": "PULSE", "to": "NEDA", "type": "forward" }, { "from": "NEDA", "to": "HMC", "type": "forward" }, { "from": "HMC", "to": "ECHO", "type": "forward" }, { "from": "ECHO", "to": "CIG", "type": "forward" }, { "from": "CIG", "to": "HDR", "type": "forward" }, { "from": "HDR", "to": "QIPC", "type": "forward" }, { "from": "QIPC", "to": "META", "type": "forward" }, { "from": "META", "to": "IMMU", "type": "forward" }, { "from": "IMMU", "to": "NDS", "type": "forward" } ] };} /** * Create text sprite for room labels */ function createTextSprite(text) { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = 256; canvas.height = 64; context.font = '24px Space Grotesk, Arial'; context.fillStyle = '#ffffff'; context.textAlign = 'center'; context.fillText(text, 128, 40); const texture = new THREE.CanvasTexture(canvas); const material = new THREE.SpriteMaterial({ map: texture, transparent: true }); const sprite = new THREE.Sprite(material); sprite.scale.set(3, 0.75, 1); return sprite; } /** * Show room info panel */ function showRoomInfo(room) { let panel = document.getElementById('room-info'); if (!panel) { panel = document.createElement('div'); panel.id = 'room-info'; panel.style.cssText = ` position: fixed; top: 20px; right: 20px; width: 280px; padding: 20px; background: rgba(10, 10, 18, 0.95); border: 1px solid rgba(139, 92, 246, 0.4); border-radius: 12px; color: #fff; font-family: 'Space Grotesk', sans-serif; z-index: 100; backdrop-filter: blur(10px); `; document.body.appendChild(panel); } const typeColors = { input: '#8b5cf6', processing: '#a855f7', quorum: '#d946ef', memory: '#e879f9', output: '#ec4899' }; panel.innerHTML = `

${room.label}

Type: ${room.type}

ID: ${room.id}

Capacity: ${room.capacity || 100}

`; selectedRoom = room; } /** * Hide room info panel */ function hideRoomInfo() { const panel = document.getElementById('room-info'); if (panel) panel.remove(); selectedRoom = null; } /** * Start cinematic tour (legacy compatibility) */ function startCinematicTour() { Tour.toggle(camera, { x: 0, y: 0, z: 0 }); } // Initialize on DOM ready - handle full initialization document.addEventListener('DOMContentLoaded', () => { // Initialize scene first initScene(); // Then load config with fallback let loadingHidden = false; const hideLoadingWhenReady = () => { if (loadingHidden) return; loadingHidden = true; hideLoading(); }; // Timeout to hide loading after 3 seconds even if something fails const loadingTimeout = setTimeout(() => { console.warn('[Palace] Loading timeout, forcing hide'); hideLoadingWhenReady(); }, 3000); fetch('palace.json') .then(res => { if (!res.ok) throw new Error('Config not found: ' + res.status); return res.json(); }) .then(config => { console.log('[Palace] Loaded config:', config.name); try { loadPalace(config); // If loadPalace succeeded, hide loading after a short delay to allow first render setTimeout(hideLoadingWhenReady, 0); } catch (e) { console.error('[Palace] Error loading config:', e); // Fallback to default config loadPalace(getDefaultConfig()); hideLoadingWhenReady(); } }) .catch(err => { console.log('[Palace] Using default config due to:', err); loadPalace(getDefaultConfig()); hideLoadingWhenReady(); }); }); /** * Hide loading overlay */ function hideLoading() { const overlay = document.getElementById('loading-overlay'); if (overlay) { overlay.style.opacity = '0'; setTimeout(() => overlay.remove(), 500); } }