Compare commits
3 Commits
mimo/code/
...
feat/mnemo
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a7e554568 | |||
| d12bd7a806 | |||
| 9355c02417 |
16
app.js
16
app.js
@@ -6,6 +6,7 @@ import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
|
|||||||
import { SpatialMemory } from './nexus/components/spatial-memory.js';
|
import { SpatialMemory } from './nexus/components/spatial-memory.js';
|
||||||
import { MemoryBirth } from './nexus/components/memory-birth.js';
|
import { MemoryBirth } from './nexus/components/memory-birth.js';
|
||||||
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
|
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
|
||||||
|
import { MemoryPulse } from './nexus/components/memory-pulse.js';
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// NEXUS v1.1 — Portal System Update
|
// NEXUS v1.1 — Portal System Update
|
||||||
@@ -710,6 +711,7 @@ async function init() {
|
|||||||
createAshStorm();
|
createAshStorm();
|
||||||
SpatialMemory.init(scene);
|
SpatialMemory.init(scene);
|
||||||
MemoryBirth.init(scene);
|
MemoryBirth.init(scene);
|
||||||
|
MemoryPulse.init(scene, SpatialMemory);
|
||||||
MemoryBirth.wrapSpatialMemory(SpatialMemory);
|
MemoryBirth.wrapSpatialMemory(SpatialMemory);
|
||||||
SpatialMemory.setCamera(camera);
|
SpatialMemory.setCamera(camera);
|
||||||
updateLoad(90);
|
updateLoad(90);
|
||||||
@@ -1918,6 +1920,19 @@ function setupControls() {
|
|||||||
const portal = portals.find(p => p.ring === clickedRing);
|
const portal = portals.find(p => p.ring === clickedRing);
|
||||||
if (portal) activatePortal(portal);
|
if (portal) activatePortal(portal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Raycasting for memory crystals — trigger pulse
|
||||||
|
const crystalMeshes = SpatialMemory.getCrystalMeshes();
|
||||||
|
if (crystalMeshes.length > 0) {
|
||||||
|
const crystalHits = raycaster.intersectObjects(crystalMeshes);
|
||||||
|
if (crystalHits.length > 0) {
|
||||||
|
const hitMesh = crystalHits[0].object;
|
||||||
|
const memData = SpatialMemory.getMemoryFromMesh(hitMesh);
|
||||||
|
if (memData) {
|
||||||
|
MemoryPulse.triggerPulse(memData.data.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2872,6 +2887,7 @@ function gameLoop() {
|
|||||||
if (typeof animateMemoryOrbs === 'function') {
|
if (typeof animateMemoryOrbs === 'function') {
|
||||||
SpatialMemory.update(delta);
|
SpatialMemory.update(delta);
|
||||||
MemoryBirth.update(delta);
|
MemoryBirth.update(delta);
|
||||||
|
MemoryPulse.update(delta);
|
||||||
animateMemoryOrbs(delta);
|
animateMemoryOrbs(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
305
nexus/components/memory-pulse.js
Normal file
305
nexus/components/memory-pulse.js
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// PROJECT MNEMOSYNE — MEMORY PULSE ENGINE
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
//
|
||||||
|
// Holographic ripple propagation: when a memory crystal is accessed,
|
||||||
|
// a visual pulse wave radiates outward through the connection graph,
|
||||||
|
// illuminating linked memories in decreasing intensity by hop distance.
|
||||||
|
//
|
||||||
|
// This makes the archive feel alive — one thought echoing through
|
||||||
|
// the holographic field of related knowledge.
|
||||||
|
//
|
||||||
|
// Issue: Mnemosyne Pulse Effect
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
const MemoryPulse = (() => {
|
||||||
|
let _scene = null;
|
||||||
|
let _spatialMemory = null;
|
||||||
|
let _activePulses = []; // Currently propagating pulse waves
|
||||||
|
let _pulseRings = []; // Active ring meshes being rendered
|
||||||
|
let _connectionFlashes = []; // Active connection line flashes
|
||||||
|
|
||||||
|
const PULSE_SPEED = 8; // Units per second propagation
|
||||||
|
const PULSE_MAX_HOPS = 5; // Max graph depth to traverse
|
||||||
|
const RING_DURATION = 1.5; // Seconds each ring is visible
|
||||||
|
const RING_MAX_RADIUS = 2.0; // Max expansion of pulse ring
|
||||||
|
const FLASH_DURATION = 0.8; // Seconds connection lines flash
|
||||||
|
const BASE_INTENSITY = 3.0; // Emissive boost at pulse origin
|
||||||
|
const HOP_DECAY = 0.65; // Intensity multiplier per hop
|
||||||
|
|
||||||
|
// ─── INIT ────────────────────────────────────────────
|
||||||
|
function init(scene, spatialMemory) {
|
||||||
|
_scene = scene;
|
||||||
|
_spatialMemory = spatialMemory;
|
||||||
|
console.info('[Mnemosyne] Pulse engine initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── TRIGGER PULSE ──────────────────────────────────
|
||||||
|
/**
|
||||||
|
* Fire a pulse from a memory crystal. Propagates through
|
||||||
|
* connected memories by BFS, creating visual rings and
|
||||||
|
* connection line flashes at each hop.
|
||||||
|
* @param {string} sourceId - Memory ID to pulse from
|
||||||
|
*/
|
||||||
|
function triggerPulse(sourceId) {
|
||||||
|
if (!_scene || !_spatialMemory) return;
|
||||||
|
|
||||||
|
const memories = _spatialMemory.getAllMemories();
|
||||||
|
const source = memories.find(m => m.id === sourceId);
|
||||||
|
if (!source) return;
|
||||||
|
|
||||||
|
// BFS through connection graph
|
||||||
|
const visited = new Set();
|
||||||
|
const queue = [{ id: sourceId, hop: 0, delay: 0 }];
|
||||||
|
visited.add(sourceId);
|
||||||
|
|
||||||
|
const memMap = {};
|
||||||
|
memories.forEach(m => { memMap[m.id] = m; });
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const { id, hop, delay } = queue.shift();
|
||||||
|
if (hop > PULSE_MAX_HOPS) continue;
|
||||||
|
|
||||||
|
const mem = memMap[id];
|
||||||
|
if (!mem) continue;
|
||||||
|
|
||||||
|
// Schedule ring spawn
|
||||||
|
_scheduleRing(id, hop, delay);
|
||||||
|
|
||||||
|
// Schedule connection flashes to neighbors
|
||||||
|
const connections = mem.connections || [];
|
||||||
|
connections.forEach(targetId => {
|
||||||
|
if (visited.has(targetId)) return;
|
||||||
|
visited.add(targetId);
|
||||||
|
|
||||||
|
const target = memMap[targetId];
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const travelDelay = delay + _travelTime(mem, target);
|
||||||
|
_scheduleConnectionFlash(id, targetId, delay, travelDelay);
|
||||||
|
queue.push({ id: targetId, hop: hop + 1, delay: travelDelay });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── TRAVEL TIME ────────────────────────────────────
|
||||||
|
function _travelTime(src, dst) {
|
||||||
|
const sp = src.position || [0, 0, 0];
|
||||||
|
const dp = dst.position || [0, 0, 0];
|
||||||
|
const dx = sp[0] - dp[0], dy = sp[1] - dp[1], dz = sp[2] - dp[2];
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
return dist / PULSE_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── SCHEDULE RING ──────────────────────────────────
|
||||||
|
function _scheduleRing(memId, hop, delay) {
|
||||||
|
const startTime = performance.now() + delay * 1000;
|
||||||
|
_activePulses.push({
|
||||||
|
type: 'ring',
|
||||||
|
memId,
|
||||||
|
hop,
|
||||||
|
startTime,
|
||||||
|
duration: RING_DURATION,
|
||||||
|
intensity: BASE_INTENSITY * Math.pow(HOP_DECAY, hop),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── SCHEDULE CONNECTION FLASH ─────────────────────
|
||||||
|
function _scheduleConnectionFlash(fromId, toId, startDelay, endDelay) {
|
||||||
|
const startTime = performance.now() + startDelay * 1000;
|
||||||
|
_activePulses.push({
|
||||||
|
type: 'flash',
|
||||||
|
fromId,
|
||||||
|
toId,
|
||||||
|
startTime,
|
||||||
|
duration: endDelay - startDelay + FLASH_DURATION,
|
||||||
|
intensity: BASE_INTENSITY * Math.pow(HOP_DECAY, 0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── UPDATE (called per frame) ──────────────────────
|
||||||
|
function update(delta) {
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
// Process scheduled pulses
|
||||||
|
for (let i = _activePulses.length - 1; i >= 0; i--) {
|
||||||
|
const pulse = _activePulses[i];
|
||||||
|
if (now < pulse.startTime) continue; // Not yet active
|
||||||
|
|
||||||
|
const elapsed = (now - pulse.startTime) / 1000;
|
||||||
|
const progress = Math.min(1, elapsed / pulse.duration);
|
||||||
|
|
||||||
|
if (progress >= 1) {
|
||||||
|
_activePulses.splice(i, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pulse.type === 'ring') {
|
||||||
|
_renderRing(pulse, elapsed, progress);
|
||||||
|
} else if (pulse.type === 'flash') {
|
||||||
|
_renderConnectionFlash(pulse, elapsed, progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing ring meshes
|
||||||
|
for (let i = _pulseRings.length - 1; i >= 0; i--) {
|
||||||
|
const ring = _pulseRings[i];
|
||||||
|
ring.age += delta;
|
||||||
|
|
||||||
|
if (ring.age >= ring.maxAge) {
|
||||||
|
if (ring.mesh.parent) ring.mesh.parent.remove(ring.mesh);
|
||||||
|
ring.mesh.geometry.dispose();
|
||||||
|
ring.mesh.material.dispose();
|
||||||
|
_pulseRings.splice(i, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = ring.age / ring.maxAge;
|
||||||
|
const scale = 1 + t * RING_MAX_RADIUS;
|
||||||
|
ring.mesh.scale.set(scale, scale, scale);
|
||||||
|
ring.mesh.material.opacity = ring.baseOpacity * (1 - t * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update connection flashes
|
||||||
|
for (let i = _connectionFlashes.length - 1; i >= 0; i--) {
|
||||||
|
const flash = _connectionFlashes[i];
|
||||||
|
flash.age += delta;
|
||||||
|
|
||||||
|
if (flash.age >= flash.maxAge) {
|
||||||
|
// Restore original material
|
||||||
|
if (flash.line && flash.line.material) {
|
||||||
|
flash.line.material.opacity = flash.originalOpacity;
|
||||||
|
flash.line.material.color.setHex(flash.originalColor);
|
||||||
|
}
|
||||||
|
_connectionFlashes.splice(i, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = flash.age / flash.maxAge;
|
||||||
|
if (flash.line && flash.line.material) {
|
||||||
|
// Pulse opacity with travel effect
|
||||||
|
const wave = Math.sin(t * Math.PI);
|
||||||
|
flash.line.material.opacity = flash.originalOpacity + wave * 0.6;
|
||||||
|
flash.line.material.color.setHex(
|
||||||
|
_lerpColor(flash.originalColor, flash.flashColor, wave * 0.8)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── RENDER RING ────────────────────────────────────
|
||||||
|
function _renderRing(pulse, elapsed, progress) {
|
||||||
|
// Find crystal position
|
||||||
|
const allMeshes = _spatialMemory.getCrystalMeshes();
|
||||||
|
let sourceMesh = null;
|
||||||
|
const memories = _spatialMemory.getAllMemories();
|
||||||
|
for (const mem of memories) {
|
||||||
|
if (mem.id === pulse.memId) {
|
||||||
|
// Find matching mesh
|
||||||
|
sourceMesh = allMeshes.find(m => m.userData.memId === pulse.memId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sourceMesh) return;
|
||||||
|
|
||||||
|
// Only create ring once (check if we already have one for this pulse)
|
||||||
|
if (pulse._ringCreated) return;
|
||||||
|
pulse._ringCreated = true;
|
||||||
|
|
||||||
|
const ringGeo = new THREE.RingGeometry(0.1, 0.15, 32);
|
||||||
|
const region = memories.find(m => m.id === pulse.memId);
|
||||||
|
const color = _getRegionColor(region ? region.category : 'working');
|
||||||
|
|
||||||
|
const ringMat = new THREE.MeshBasicMaterial({
|
||||||
|
color: color,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.8 * pulse.intensity / BASE_INTENSITY,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
depthWrite: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ring = new THREE.Mesh(ringGeo, ringMat);
|
||||||
|
ring.position.copy(sourceMesh.position);
|
||||||
|
ring.position.y += 0.1; // Slight offset above crystal
|
||||||
|
ring.rotation.x = -Math.PI / 2; // Flat on XZ plane
|
||||||
|
ring.lookAt(ring.position.x, ring.position.y + 1, ring.position.z);
|
||||||
|
|
||||||
|
_scene.add(ring);
|
||||||
|
|
||||||
|
_pulseRings.push({
|
||||||
|
mesh: ring,
|
||||||
|
age: 0,
|
||||||
|
maxAge: RING_DURATION,
|
||||||
|
baseOpacity: ringMat.opacity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── RENDER CONNECTION FLASH ────────────────────────
|
||||||
|
function _renderConnectionFlash(pulse, elapsed, progress) {
|
||||||
|
if (pulse._flashCreated) return;
|
||||||
|
|
||||||
|
// Find the connection line between from and to
|
||||||
|
const fromMesh = _findMesh(pulse.fromId);
|
||||||
|
const toMesh = _findMesh(pulse.toId);
|
||||||
|
if (!fromMesh || !toMesh) return;
|
||||||
|
|
||||||
|
// Create a temporary line for the flash
|
||||||
|
const points = [fromMesh.position.clone(), toMesh.position.clone()];
|
||||||
|
const geo = new THREE.BufferGeometry().setFromPoints(points);
|
||||||
|
const mat = new THREE.LineBasicMaterial({
|
||||||
|
color: 0x4af0c0,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.0,
|
||||||
|
linewidth: 2,
|
||||||
|
});
|
||||||
|
const line = new THREE.Line(geo, mat);
|
||||||
|
_scene.add(line);
|
||||||
|
|
||||||
|
pulse._flashCreated = true;
|
||||||
|
|
||||||
|
_connectionFlashes.push({
|
||||||
|
line,
|
||||||
|
age: 0,
|
||||||
|
maxAge: pulse.duration,
|
||||||
|
originalOpacity: 0.0,
|
||||||
|
originalColor: 0x334455,
|
||||||
|
flashColor: 0x4af0c0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── HELPERS ────────────────────────────────────────
|
||||||
|
function _findMesh(memId) {
|
||||||
|
const meshes = _spatialMemory.getCrystalMeshes();
|
||||||
|
return meshes.find(m => m.userData.memId === memId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getRegionColor(category) {
|
||||||
|
const colors = {
|
||||||
|
documents: 0x4af0c0,
|
||||||
|
projects: 0xff6b35,
|
||||||
|
code: 0x7b5cff,
|
||||||
|
social: 0xff4488,
|
||||||
|
working: 0xffd700,
|
||||||
|
archive: 0x445566,
|
||||||
|
};
|
||||||
|
return colors[category] || colors.working;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _lerpColor(a, b, t) {
|
||||||
|
const ar = (a >> 16) & 0xff, ag = (a >> 8) & 0xff, ab = a & 0xff;
|
||||||
|
const br = (b >> 16) & 0xff, bg = (b >> 8) & 0xff, bb = b & 0xff;
|
||||||
|
const rr = Math.round(ar + (br - ar) * t);
|
||||||
|
const rg = Math.round(ag + (bg - ag) * t);
|
||||||
|
const rb = Math.round(ab + (bb - ab) * t);
|
||||||
|
return (rr << 16) | (rg << 8) | rb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PUBLIC API ─────────────────────────────────────
|
||||||
|
return {
|
||||||
|
init,
|
||||||
|
triggerPulse,
|
||||||
|
update,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
export { MemoryPulse };
|
||||||
Reference in New Issue
Block a user