Compare commits
2 Commits
fix/mnemos
...
feat/memor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6b7d9137b | ||
|
|
8d7930de31 |
3
app.js
3
app.js
@@ -7,6 +7,7 @@ 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 { MemoryInspect } from './nexus/components/memory-inspect.js';
|
||||
import { MemoryPulse } from './nexus/components/memory-pulse.js';
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// NEXUS v1.1 — Portal System Update
|
||||
@@ -715,6 +716,7 @@ async function init() {
|
||||
MemoryBirth.wrapSpatialMemory(SpatialMemory);
|
||||
SpatialMemory.setCamera(camera);
|
||||
MemoryInspect.init({ onNavigate: _navigateToMemory });
|
||||
MemoryPulse.init(scene);
|
||||
updateLoad(90);
|
||||
|
||||
loadSession();
|
||||
@@ -1947,6 +1949,7 @@ function setupControls() {
|
||||
SpatialMemory.highlightMemory(entry.data.id);
|
||||
const regionDef = SpatialMemory.REGIONS[entry.region] || SpatialMemory.REGIONS.working;
|
||||
MemoryInspect.show(entry.data, regionDef);
|
||||
MemoryPulse.trigger(entry.data.id, SpatialMemory);
|
||||
}
|
||||
} else {
|
||||
// Clicked empty space — close inspect panel and deselect crystal
|
||||
|
||||
256
nexus/components/memory-pulse.js
Normal file
256
nexus/components/memory-pulse.js
Normal file
@@ -0,0 +1,256 @@
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// MNEMOSYNE — Memory Pulse
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
//
|
||||
// Visual pulse wave that radiates through the connection graph
|
||||
// when a memory crystal is clicked. Illuminates linked memories
|
||||
// by BFS hop distance — closer neighbors light up first.
|
||||
//
|
||||
// Usage from app.js:
|
||||
// import { MemoryPulse } from './nexus/components/memory-pulse.js';
|
||||
// MemoryPulse.init(scene);
|
||||
// MemoryPulse.trigger(clickedMemId, SpatialMemory);
|
||||
//
|
||||
// Depends on: SpatialMemory (getAllMemories, getMemoryFromMesh)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
const MemoryPulse = (() => {
|
||||
let _scene = null;
|
||||
let _activePulses = []; // track running animations for cleanup
|
||||
|
||||
const HOP_DELAY = 300; // ms between each BFS hop wave
|
||||
const GLOW_DURATION = 800; // ms each crystal glows at peak
|
||||
const FADE_DURATION = 600; // ms to fade back to normal
|
||||
const PULSE_COLOR = 0x4af0c0; // cyan-green pulse glow
|
||||
const PULSE_INTENSITY = 6.0; // peak emissive during pulse
|
||||
const RING_DURATION = 1200; // ms for the expanding ring effect
|
||||
|
||||
// ─── INIT ────────────────────────────────────────────────
|
||||
function init(scene) {
|
||||
_scene = scene;
|
||||
}
|
||||
|
||||
// ─── BFS TRAVERSAL ───────────────────────────────────────
|
||||
// Returns array of arrays: [[hop-0 ids], [hop-1 ids], [hop-2 ids], ...]
|
||||
function bfsHops(startId, allMemories) {
|
||||
const memMap = {};
|
||||
for (const m of allMemories) {
|
||||
memMap[m.id] = m;
|
||||
}
|
||||
|
||||
if (!memMap[startId]) return [];
|
||||
|
||||
const visited = new Set([startId]);
|
||||
const hops = [];
|
||||
let frontier = [startId];
|
||||
|
||||
while (frontier.length > 0) {
|
||||
hops.push([...frontier]);
|
||||
const next = [];
|
||||
for (const id of frontier) {
|
||||
const mem = memMap[id];
|
||||
if (!mem || !mem.connections) continue;
|
||||
for (const connId of mem.connections) {
|
||||
if (!visited.has(connId)) {
|
||||
visited.add(connId);
|
||||
next.push(connId);
|
||||
}
|
||||
}
|
||||
}
|
||||
frontier = next;
|
||||
}
|
||||
|
||||
return hops;
|
||||
}
|
||||
|
||||
// ─── EXPANDING RING ──────────────────────────────────────
|
||||
// Creates a flat ring geometry that expands outward from a position
|
||||
function createExpandingRing(position, color) {
|
||||
const ringGeo = new THREE.RingGeometry(0.1, 0.2, 32);
|
||||
const ringMat = new THREE.MeshBasicMaterial({
|
||||
color: color,
|
||||
transparent: true,
|
||||
opacity: 0.8,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false
|
||||
});
|
||||
const ring = new THREE.Mesh(ringGeo, ringMat);
|
||||
ring.position.copy(position);
|
||||
ring.position.y += 0.1; // slightly above crystal
|
||||
ring.rotation.x = -Math.PI / 2; // flat horizontal
|
||||
ring.scale.set(0.1, 0.1, 0.1);
|
||||
_scene.add(ring);
|
||||
return ring;
|
||||
}
|
||||
|
||||
// ─── ANIMATE RING ────────────────────────────────────────
|
||||
function animateRing(ring, onComplete) {
|
||||
const startTime = performance.now();
|
||||
function tick() {
|
||||
const elapsed = performance.now() - startTime;
|
||||
const t = Math.min(1, elapsed / RING_DURATION);
|
||||
|
||||
// Expand outward
|
||||
const scale = 0.1 + t * 4.0;
|
||||
ring.scale.set(scale, scale, scale);
|
||||
|
||||
// Fade out
|
||||
ring.material.opacity = 0.8 * (1 - t * t);
|
||||
|
||||
if (t < 1) {
|
||||
requestAnimationFrame(tick);
|
||||
} else {
|
||||
_scene.remove(ring);
|
||||
ring.geometry.dispose();
|
||||
ring.material.dispose();
|
||||
if (onComplete) onComplete();
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
// ─── PULSE CRYSTAL GLOW ──────────────────────────────────
|
||||
// Temporarily boosts a crystal's emissive intensity
|
||||
function pulseGlow(mesh, hopIndex) {
|
||||
if (!mesh || !mesh.material) return;
|
||||
|
||||
const originalIntensity = mesh.material.emissiveIntensity;
|
||||
const originalColor = mesh.material.emissive ? mesh.material.emissive.clone() : null;
|
||||
const delay = hopIndex * HOP_DELAY;
|
||||
|
||||
setTimeout(() => {
|
||||
if (!mesh.material) return;
|
||||
|
||||
// Store original for restore
|
||||
const origInt = mesh.material.emissiveIntensity;
|
||||
|
||||
// Flash to pulse color
|
||||
if (mesh.material.emissive) {
|
||||
mesh.material.emissive.setHex(PULSE_COLOR);
|
||||
}
|
||||
mesh.material.emissiveIntensity = PULSE_INTENSITY;
|
||||
|
||||
// Also boost point light if present
|
||||
let origLightIntensity = null;
|
||||
let origLightColor = null;
|
||||
if (mesh.children) {
|
||||
for (const child of mesh.children) {
|
||||
if (child.isPointLight) {
|
||||
origLightIntensity = child.intensity;
|
||||
origLightColor = child.color.clone();
|
||||
child.intensity = 3.0;
|
||||
child.color.setHex(PULSE_COLOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hold at peak, then fade
|
||||
setTimeout(() => {
|
||||
const fadeStart = performance.now();
|
||||
function fadeTick() {
|
||||
const elapsed = performance.now() - fadeStart;
|
||||
const t = Math.min(1, elapsed / FADE_DURATION);
|
||||
const eased = 1 - (1 - t) * (1 - t); // ease-out quad
|
||||
|
||||
mesh.material.emissiveIntensity = PULSE_INTENSITY + (origInt - PULSE_INTENSITY) * eased;
|
||||
|
||||
if (originalColor) {
|
||||
const pr = ((PULSE_COLOR >> 16) & 0xff) / 255;
|
||||
const pg = ((PULSE_COLOR >> 8) & 0xff) / 255;
|
||||
const pb = (PULSE_COLOR & 0xff) / 255;
|
||||
mesh.material.emissive.setRGB(
|
||||
pr + (originalColor.r - pr) * eased,
|
||||
pg + (originalColor.g - pg) * eased,
|
||||
pb + (originalColor.b - pb) * eased
|
||||
);
|
||||
}
|
||||
|
||||
// Restore point light
|
||||
if (origLightIntensity !== null && mesh.children) {
|
||||
for (const child of mesh.children) {
|
||||
if (child.isPointLight) {
|
||||
child.intensity = 3.0 + (origLightIntensity - 3.0) * eased;
|
||||
if (origLightColor) {
|
||||
const pr = ((PULSE_COLOR >> 16) & 0xff) / 255;
|
||||
const pg = ((PULSE_COLOR >> 8) & 0xff) / 255;
|
||||
const pb = (PULSE_COLOR & 0xff) / 255;
|
||||
child.color.setRGB(
|
||||
pr + (origLightColor.r - pr) * eased,
|
||||
pg + (origLightColor.g - pg) * eased,
|
||||
pb + (origLightColor.b - pb) * eased
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (t < 1) {
|
||||
requestAnimationFrame(fadeTick);
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(fadeTick);
|
||||
}, GLOW_DURATION);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// ─── TRIGGER ─────────────────────────────────────────────
|
||||
// Main entry point: fire a pulse wave from the given memory ID
|
||||
function trigger(memId, spatialMemory) {
|
||||
if (!_scene) return;
|
||||
|
||||
const allMemories = spatialMemory.getAllMemories();
|
||||
const hops = bfsHops(memId, allMemories);
|
||||
|
||||
if (hops.length <= 1) {
|
||||
// No connections — just do a local ring
|
||||
const obj = spatialMemory.getMemoryFromMesh(
|
||||
spatialMemory.getCrystalMeshes().find(m => m.userData.memId === memId)
|
||||
);
|
||||
if (obj && obj.mesh) {
|
||||
const ring = createExpandingRing(obj.mesh.position, PULSE_COLOR);
|
||||
animateRing(ring);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For each hop level, create expanding rings and pulse glows
|
||||
for (let hopIdx = 0; hopIdx < hops.length; hopIdx++) {
|
||||
const idsInHop = hops[hopIdx];
|
||||
|
||||
for (const id of idsInHop) {
|
||||
// Find mesh for this memory
|
||||
const meshes = spatialMemory.getCrystalMeshes();
|
||||
let targetMesh = null;
|
||||
for (const m of meshes) {
|
||||
if (m.userData && m.userData.memId === id) {
|
||||
targetMesh = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetMesh) continue;
|
||||
|
||||
// Schedule pulse glow
|
||||
pulseGlow(targetMesh, hopIdx);
|
||||
|
||||
// Create expanding ring at this hop's delay
|
||||
((mesh, delay) => {
|
||||
setTimeout(() => {
|
||||
const ring = createExpandingRing(mesh.position, PULSE_COLOR);
|
||||
animateRing(ring);
|
||||
}, delay * HOP_DELAY);
|
||||
})(targetMesh, hopIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── CLEANUP ─────────────────────────────────────────────
|
||||
function dispose() {
|
||||
// Active pulses will self-clean via their animation callbacks
|
||||
_activePulses = [];
|
||||
}
|
||||
|
||||
return { init, trigger, dispose, bfsHops };
|
||||
})();
|
||||
|
||||
export { MemoryPulse };
|
||||
Reference in New Issue
Block a user