Compare commits
3 Commits
mimo/creat
...
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 { MemoryBirth } from './nexus/components/memory-birth.js';
|
||||
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
|
||||
import { MemoryPulse } from './nexus/components/memory-pulse.js';
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// NEXUS v1.1 — Portal System Update
|
||||
@@ -710,6 +711,7 @@ async function init() {
|
||||
createAshStorm();
|
||||
SpatialMemory.init(scene);
|
||||
MemoryBirth.init(scene);
|
||||
MemoryPulse.init(scene, SpatialMemory);
|
||||
MemoryBirth.wrapSpatialMemory(SpatialMemory);
|
||||
SpatialMemory.setCamera(camera);
|
||||
updateLoad(90);
|
||||
@@ -1918,6 +1920,19 @@ function setupControls() {
|
||||
const portal = portals.find(p => p.ring === clickedRing);
|
||||
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') {
|
||||
SpatialMemory.update(delta);
|
||||
MemoryBirth.update(delta);
|
||||
MemoryPulse.update(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