161 lines
5.1 KiB
JavaScript
161 lines
5.1 KiB
JavaScript
// ═══════════════════════════════════════════════════
|
|
// 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 };
|