// ═══════════════════════════════════════════════════ // PROJECT MNEMOSYNE — MEMORY PULSE // ═══════════════════════════════════════════════════ // // BFS wave animation triggered on crystal click. // When a memory crystal is clicked, a visual pulse // radiates through the connection graph — illuminating // linked memories hop-by-hop with a glow that rises // sharply and then fades. // // Usage: // MemoryPulse.init(SpatialMemory); // MemoryPulse.triggerPulse(memId); // MemoryPulse.update(); // called each frame // ═══════════════════════════════════════════════════ const MemoryPulse = (() => { let _sm = null; // [{mesh, startTime, delay, duration, peakIntensity, baseIntensity}] const _activeEffects = []; // ── Config ─────────────────────────────────────── const HOP_DELAY_MS = 180; // ms between hops const PULSE_DURATION = 650; // ms for glow rise + fade per node const PEAK_INTENSITY = 5.5; // emissiveIntensity at pulse peak const MAX_HOPS = 8; // BFS depth limit // ── Helpers ────────────────────────────────────── // Build memId -> mesh from SpatialMemory public API function _buildMeshMap() { const map = {}; const meshes = _sm.getCrystalMeshes(); for (const mesh of meshes) { const entry = _sm.getMemoryFromMesh(mesh); if (entry) map[entry.data.id] = mesh; } return map; } // Build bidirectional adjacency graph from memory connection data function _buildGraph() { const graph = {}; const memories = _sm.getAllMemories(); for (const mem of memories) { if (!graph[mem.id]) graph[mem.id] = []; if (mem.connections) { for (const targetId of mem.connections) { graph[mem.id].push(targetId); if (!graph[targetId]) graph[targetId] = []; graph[targetId].push(mem.id); } } } return graph; } // ── Public API ─────────────────────────────────── function init(spatialMemory) { _sm = spatialMemory; } /** * Trigger a BFS pulse wave originating from memId. * Each hop level illuminates after HOP_DELAY_MS * hop ms. * @param {string} memId - ID of the clicked memory crystal */ function triggerPulse(memId) { if (!_sm) return; const meshMap = _buildMeshMap(); const graph = _buildGraph(); if (!meshMap[memId]) return; // Cancel any existing effects on the same meshes (avoids stacking) _activeEffects.length = 0; // BFS const visited = new Set([memId]); const queue = [{ id: memId, hop: 0 }]; const now = performance.now(); const scheduled = []; while (queue.length > 0) { const { id, hop } = queue.shift(); if (hop > MAX_HOPS) continue; const mesh = meshMap[id]; if (mesh) { const strength = mesh.userData.strength || 0.7; const baseIntensity = 1.0 + Math.sin(mesh.userData.pulse || 0) * 0.5 * strength; scheduled.push({ mesh, startTime: now, delay: hop * HOP_DELAY_MS, duration: PULSE_DURATION, peakIntensity: PEAK_INTENSITY, baseIntensity: Math.max(0.5, baseIntensity) }); } for (const neighborId of (graph[id] || [])) { if (!visited.has(neighborId)) { visited.add(neighborId); queue.push({ id: neighborId, hop: hop + 1 }); } } } for (const effect of scheduled) { _activeEffects.push(effect); } console.info('[MemoryPulse] Pulse triggered from', memId, '—', scheduled.length, 'nodes in wave'); } /** * Advance all active pulse animations. Call once per frame. */ function update() { if (_activeEffects.length === 0) return; const now = performance.now(); for (let i = _activeEffects.length - 1; i >= 0; i--) { const e = _activeEffects[i]; const elapsed = now - e.startTime - e.delay; if (elapsed < 0) continue; // waiting for its hop delay if (elapsed >= e.duration) { // Animation complete — restore base intensity if (e.mesh.material) { e.mesh.material.emissiveIntensity = e.baseIntensity; } _activeEffects.splice(i, 1); continue; } // t: 0 → 1 over duration const t = elapsed / e.duration; // sin curve over [0, π]: smooth rise then fall const glow = Math.sin(t * Math.PI); if (e.mesh.material) { e.mesh.material.emissiveIntensity = e.baseIntensity + glow * (e.peakIntensity - e.baseIntensity); } } } return { init, triggerPulse, update }; })(); export { MemoryPulse };