// ═══════════════════════════════════════════ // PROJECT MNEMOSYNE — AMBIENT PARTICLE SYSTEM // ═══════════════════════════════════════════ // // Memory activity visualization via Three.js Points. // Three particle modes: // 1. Spawn burst — 20 particles on new fact, 2s fade // 2. Access trail — 10 particles streaming to crystal // 3. Ambient dust — 200 particles, slow cosmic drift // // Category colors for all particles. // Total budget: < 500 particles at any time. // // Usage from app.js: // import { MemoryParticles } from './nexus/components/memory-particles.js'; // MemoryParticles.init(scene); // MemoryParticles.onMemoryPlaced(position, category); // MemoryParticles.onMemoryAccessed(fromPos, toPos, category); // MemoryParticles.update(delta); // ═══════════════════════════════════════════ const MemoryParticles = (() => { let _scene = null; let _initialized = false; // ─── CATEGORY COLORS ────────────────────── const CATEGORY_COLORS = { engineering: new THREE.Color(0x4af0c0), social: new THREE.Color(0x7b5cff), knowledge: new THREE.Color(0xffd700), projects: new THREE.Color(0xff4466), working: new THREE.Color(0x00ff88), archive: new THREE.Color(0x334455), user_pref: new THREE.Color(0xffd700), project: new THREE.Color(0x4488ff), tool_knowledge: new THREE.Color(0x44ff88), general: new THREE.Color(0x8899aa), }; const DEFAULT_COLOR = new THREE.Color(0x8899bb); // ─── PARTICLE BUDGETS ───────────────────── const MAX_BURST_PARTICLES = 20; // per spawn event const MAX_TRAIL_PARTICLES = 10; // per access event const AMBIENT_COUNT = 200; // always-on dust const MAX_ACTIVE_BURSTS = 8; // max concurrent burst groups const MAX_ACTIVE_TRAILS = 5; // max concurrent trail groups // ─── ACTIVE PARTICLE GROUPS ─────────────── let _bursts = []; // { points, velocities, life, maxLife } let _trails = []; // { points, velocities, life, maxLife, target } let _ambientPoints = null; // ─── HELPERS ────────────────────────────── function _getCategoryColor(category) { return CATEGORY_COLORS[category] || DEFAULT_COLOR; } // ═══ AMBIENT DUST ═════════════════════════ function _createAmbient() { const geo = new THREE.BufferGeometry(); const positions = new Float32Array(AMBIENT_COUNT * 3); const colors = new Float32Array(AMBIENT_COUNT * 3); const sizes = new Float32Array(AMBIENT_COUNT); // Distribute across the world for (let i = 0; i < AMBIENT_COUNT; i++) { positions[i * 3] = (Math.random() - 0.5) * 50; positions[i * 3 + 1] = Math.random() * 18 + 1; positions[i * 3 + 2] = (Math.random() - 0.5) * 50; // Subtle category-tinted colors const categories = Object.keys(CATEGORY_COLORS); const cat = categories[Math.floor(Math.random() * categories.length)]; const col = _getCategoryColor(cat).clone().multiplyScalar(0.4 + Math.random() * 0.3); colors[i * 3] = col.r; colors[i * 3 + 1] = col.g; colors[i * 3 + 2] = col.b; sizes[i] = 0.02 + Math.random() * 0.04; } geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geo.setAttribute('color', new THREE.BufferAttribute(colors, 3)); geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); const mat = new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 } }, vertexShader: ` attribute float size; attribute vec3 color; varying vec3 vColor; varying float vAlpha; uniform float uTime; void main() { vColor = color; vec3 pos = position; // Slow cosmic drift pos.x += sin(uTime * 0.08 + position.y * 0.3) * 0.5; pos.y += sin(uTime * 0.05 + position.z * 0.2) * 0.3; pos.z += cos(uTime * 0.06 + position.x * 0.25) * 0.4; vec4 mv = modelViewMatrix * vec4(pos, 1.0); gl_PointSize = size * 250.0 / -mv.z; gl_Position = projectionMatrix * mv; // Fade with distance vAlpha = smoothstep(40.0, 10.0, -mv.z) * 0.5; } `, fragmentShader: ` varying vec3 vColor; varying float vAlpha; void main() { float d = length(gl_PointCoord - 0.5); if (d > 0.5) discard; float alpha = smoothstep(0.5, 0.05, d); gl_FragColor = vec4(vColor, alpha * vAlpha); } `, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, }); _ambientPoints = new THREE.Points(geo, mat); _scene.add(_ambientPoints); } // ═══ BURST EFFECT ═════════════════════════ function _createBurst(position, category) { const count = MAX_BURST_PARTICLES; const geo = new THREE.BufferGeometry(); const positions = new Float32Array(count * 3); const colors = new Float32Array(count * 3); const sizes = new Float32Array(count); const velocities = []; const col = _getCategoryColor(category); for (let i = 0; i < count; i++) { positions[i * 3] = position.x; positions[i * 3 + 1] = position.y; positions[i * 3 + 2] = position.z; colors[i * 3] = col.r; colors[i * 3 + 1] = col.g; colors[i * 3 + 2] = col.b; sizes[i] = 0.06 + Math.random() * 0.06; // Random outward velocity const theta = Math.random() * Math.PI * 2; const phi = Math.random() * Math.PI; const speed = 1.5 + Math.random() * 2.5; velocities.push( Math.sin(phi) * Math.cos(theta) * speed, Math.cos(phi) * speed * 0.8 + 1.0, // bias upward Math.sin(phi) * Math.sin(theta) * speed ); } geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geo.setAttribute('color', new THREE.BufferAttribute(colors, 3)); geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); const mat = new THREE.ShaderMaterial({ uniforms: { uOpacity: { value: 1.0 } }, vertexShader: ` attribute float size; attribute vec3 color; varying vec3 vColor; uniform float uOpacity; void main() { vColor = color; vec4 mv = modelViewMatrix * vec4(position, 1.0); gl_PointSize = size * 300.0 / -mv.z; gl_Position = projectionMatrix * mv; } `, fragmentShader: ` varying vec3 vColor; uniform float uOpacity; void main() { float d = length(gl_PointCoord - 0.5); if (d > 0.5) discard; float alpha = smoothstep(0.5, 0.05, d); gl_FragColor = vec4(vColor, alpha * uOpacity); } `, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, }); const points = new THREE.Points(geo, mat); _scene.add(points); _bursts.push({ points, velocities, life: 0, maxLife: 2.0, // 2s fade }); // Cap active bursts while (_bursts.length > MAX_ACTIVE_BURSTS) { _removeBurst(0); } } function _removeBurst(idx) { const burst = _bursts[idx]; if (burst.points.parent) burst.points.parent.remove(burst.points); burst.points.geometry.dispose(); burst.points.material.dispose(); _bursts.splice(idx, 1); } // ═══ TRAIL EFFECT ═════════════════════════ function _createTrail(fromPos, toPos, category) { const count = MAX_TRAIL_PARTICLES; const geo = new THREE.BufferGeometry(); const positions = new Float32Array(count * 3); const colors = new Float32Array(count * 3); const sizes = new Float32Array(count); const velocities = []; const col = _getCategoryColor(category); for (let i = 0; i < count; i++) { // Stagger start positions along the path const t = Math.random(); positions[i * 3] = fromPos.x + (toPos.x - fromPos.x) * t + (Math.random() - 0.5) * 0.5; positions[i * 3 + 1] = fromPos.y + (toPos.y - fromPos.y) * t + (Math.random() - 0.5) * 0.5; positions[i * 3 + 2] = fromPos.z + (toPos.z - fromPos.z) * t + (Math.random() - 0.5) * 0.5; colors[i * 3] = col.r; colors[i * 3 + 1] = col.g; colors[i * 3 + 2] = col.b; sizes[i] = 0.04 + Math.random() * 0.04; // Velocity toward target with slight randomness const dx = toPos.x - fromPos.x; const dy = toPos.y - fromPos.y; const dz = toPos.z - fromPos.z; const len = Math.sqrt(dx * dx + dy * dy + dz * dz) || 1; const speed = 2.0 + Math.random() * 1.5; velocities.push( (dx / len) * speed + (Math.random() - 0.5) * 0.5, (dy / len) * speed + (Math.random() - 0.5) * 0.5, (dz / len) * speed + (Math.random() - 0.5) * 0.5 ); } geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geo.setAttribute('color', new THREE.BufferAttribute(colors, 3)); geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); const mat = new THREE.ShaderMaterial({ uniforms: { uOpacity: { value: 1.0 } }, vertexShader: ` attribute float size; attribute vec3 color; varying vec3 vColor; uniform float uOpacity; void main() { vColor = color; vec4 mv = modelViewMatrix * vec4(position, 1.0); gl_PointSize = size * 280.0 / -mv.z; gl_Position = projectionMatrix * mv; } `, fragmentShader: ` varying vec3 vColor; uniform float uOpacity; void main() { float d = length(gl_PointCoord - 0.5); if (d > 0.5) discard; float alpha = smoothstep(0.5, 0.05, d); gl_FragColor = vec4(vColor, alpha * uOpacity); } `, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, }); const points = new THREE.Points(geo, mat); _scene.add(points); _trails.push({ points, velocities, life: 0, maxLife: 1.5, // 1.5s trail target: toPos.clone(), }); // Cap active trails while (_trails.length > MAX_ACTIVE_TRAILS) { _removeTrail(0); } } function _removeTrail(idx) { const trail = _trails[idx]; if (trail.points.parent) trail.points.parent.remove(trail.points); trail.points.geometry.dispose(); trail.points.material.dispose(); _trails.splice(idx, 1); } // ═══ PUBLIC API ═══════════════════════════ function init(scene) { _scene = scene; _initialized = true; _createAmbient(); console.info('[Mnemosyne] Ambient particle system initialized —', AMBIENT_COUNT, 'dust particles'); } function onMemoryPlaced(position, category) { if (!_initialized) return; const pos = position instanceof THREE.Vector3 ? position : new THREE.Vector3(position.x, position.y, position.z); _createBurst(pos, category); } function onMemoryAccessed(fromPosition, toPosition, category) { if (!_initialized) return; const from = fromPosition instanceof THREE.Vector3 ? fromPosition : new THREE.Vector3(fromPosition.x, fromPosition.y, fromPosition.z); const to = toPosition instanceof THREE.Vector3 ? toPosition : new THREE.Vector3(toPosition.x, toPosition.y, toPosition.z); _createTrail(from, to, category); } function update(delta) { if (!_initialized) return; // Update ambient dust if (_ambientPoints && _ambientPoints.material.uniforms) { _ambientPoints.material.uniforms.uTime.value += delta; } // Update bursts for (let i = _bursts.length - 1; i >= 0; i--) { const burst = _bursts[i]; burst.life += delta; const t = burst.life / burst.maxLife; if (t >= 1.0) { _removeBurst(i); continue; } const pos = burst.points.geometry.attributes.position.array; for (let j = 0; j < MAX_BURST_PARTICLES; j++) { pos[j * 3] += burst.velocities[j * 3] * delta; pos[j * 3 + 1] += burst.velocities[j * 3 + 1] * delta; pos[j * 3 + 2] += burst.velocities[j * 3 + 2] * delta; // Gravity + drag burst.velocities[j * 3 + 1] -= delta * 0.5; burst.velocities[j * 3] *= 0.98; burst.velocities[j * 3 + 1] *= 0.98; burst.velocities[j * 3 + 2] *= 0.98; } burst.points.geometry.attributes.position.needsUpdate = true; burst.points.material.uniforms.uOpacity.value = 1.0 - t; } // Update trails for (let i = _trails.length - 1; i >= 0; i--) { const trail = _trails[i]; trail.life += delta; const t = trail.life / trail.maxLife; if (t >= 1.0) { _removeTrail(i); continue; } const pos = trail.points.geometry.attributes.position.array; for (let j = 0; j < MAX_TRAIL_PARTICLES; j++) { pos[j * 3] += trail.velocities[j * 3] * delta; pos[j * 3 + 1] += trail.velocities[j * 3 + 1] * delta; pos[j * 3 + 2] += trail.velocities[j * 3 + 2] * delta; } trail.points.geometry.attributes.position.needsUpdate = true; trail.points.material.uniforms.uOpacity.value = 1.0 - t * t; } } function getActiveParticleCount() { let total = AMBIENT_COUNT; _bursts.forEach(b => { total += MAX_BURST_PARTICLES; }); _trails.forEach(t => { total += MAX_TRAIL_PARTICLES; }); return total; } return { init, onMemoryPlaced, onMemoryAccessed, update, getActiveParticleCount, }; })(); export { MemoryParticles };