[claude] Add particle effects for agent interactions (#3) #13
107
js/agents.js
107
js/agents.js
@@ -1,5 +1,6 @@
|
||||
import * as THREE from 'three';
|
||||
import { AGENT_DEFS, colorToCss } from './agent-defs.js';
|
||||
import { getQualityTier } from './quality.js';
|
||||
|
||||
const agents = new Map();
|
||||
let scene;
|
||||
@@ -66,6 +67,105 @@ class Agent {
|
||||
const light = new THREE.PointLight(this.color, 1.5, 10);
|
||||
this.group.add(light);
|
||||
this.light = light;
|
||||
|
||||
this._initParticles();
|
||||
}
|
||||
|
||||
_initParticles() {
|
||||
const tier = getQualityTier();
|
||||
// Particle counts scaled by quality tier — fewer on mobile for performance
|
||||
const count = tier === 'low' ? 20 : tier === 'medium' ? 40 : 64;
|
||||
this._pCount = count;
|
||||
this._pPos = new Float32Array(count * 3); // local-space positions
|
||||
this._pVel = new Float32Array(count * 3); // velocity in units/ms
|
||||
this._pLife = new Float32Array(count); // remaining lifetime in ms (0 = dead)
|
||||
this._pEmit = 0; // fractional emit accumulator
|
||||
// Particles emitted per ms when active
|
||||
this._pRate = tier === 'low' ? 0.005 : tier === 'medium' ? 0.01 : 0.016;
|
||||
this._pLastT = null;
|
||||
|
||||
// Start all particles hidden
|
||||
for (let i = 0; i < count; i++) {
|
||||
this._pPos[i * 3 + 1] = -1000;
|
||||
this._pLife[i] = 0;
|
||||
}
|
||||
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(this._pPos, 3));
|
||||
|
||||
const mat = new THREE.PointsMaterial({
|
||||
color: this.color,
|
||||
size: tier === 'low' ? 0.2 : 0.14,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
sizeAttenuation: true,
|
||||
});
|
||||
|
||||
this._pMesh = new THREE.Points(geo, mat);
|
||||
this._pMesh.frustumCulled = false;
|
||||
this.group.add(this._pMesh);
|
||||
}
|
||||
|
||||
_spawnParticle() {
|
||||
for (let i = 0; i < this._pCount; i++) {
|
||||
if (this._pLife[i] > 0) continue;
|
||||
// Spawn near agent core
|
||||
const spread = 0.5;
|
||||
this._pPos[i * 3] = (Math.random() - 0.5) * spread;
|
||||
this._pPos[i * 3 + 1] = (Math.random() - 0.5) * spread;
|
||||
this._pPos[i * 3 + 2] = (Math.random() - 0.5) * spread;
|
||||
// Outward + upward drift (units per ms)
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const radial = 0.0008 + Math.random() * 0.0018;
|
||||
const rise = 0.001 + Math.random() * 0.003;
|
||||
this._pVel[i * 3] = Math.cos(angle) * radial;
|
||||
this._pVel[i * 3 + 1] = rise;
|
||||
this._pVel[i * 3 + 2] = Math.sin(angle) * radial;
|
||||
// Lifetime 600–1400 ms
|
||||
this._pLife[i] = 600 + Math.random() * 800;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_updateParticles(time) {
|
||||
if (!this._pMesh) return;
|
||||
|
||||
const active = this.state === 'active';
|
||||
const dt = this._pLastT === null ? 16 : Math.min(time - this._pLastT, 100);
|
||||
this._pLastT = time;
|
||||
|
||||
// Emit sparks while active
|
||||
if (active) {
|
||||
this._pEmit += dt * this._pRate;
|
||||
while (this._pEmit >= 1) {
|
||||
this._pEmit -= 1;
|
||||
this._spawnParticle();
|
||||
}
|
||||
} else {
|
||||
this._pEmit = 0;
|
||||
}
|
||||
|
||||
// Integrate existing particles
|
||||
let anyAlive = false;
|
||||
for (let i = 0; i < this._pCount; i++) {
|
||||
if (this._pLife[i] <= 0) continue;
|
||||
this._pLife[i] -= dt;
|
||||
if (this._pLife[i] <= 0) {
|
||||
this._pLife[i] = 0;
|
||||
this._pPos[i * 3 + 1] = -1000; // move off-screen
|
||||
continue;
|
||||
}
|
||||
this._pPos[i * 3] += this._pVel[i * 3] * dt;
|
||||
this._pPos[i * 3 + 1] += this._pVel[i * 3 + 1] * dt;
|
||||
this._pPos[i * 3 + 2] += this._pVel[i * 3 + 2] * dt;
|
||||
anyAlive = true;
|
||||
}
|
||||
|
||||
this._pMesh.geometry.attributes.position.needsUpdate = true;
|
||||
// Fade opacity: visible when active or particles still coasting
|
||||
this._pMesh.material.opacity = (active || anyAlive) ? 0.9 : 0;
|
||||
}
|
||||
|
||||
_buildLabel() {
|
||||
@@ -104,6 +204,8 @@ class Agent {
|
||||
this.ring.material.opacity = 0.3 + pulse * 0.2;
|
||||
|
||||
this.group.position.y = this.position.y + Math.sin(time * 0.001 + this.pulsePhase) * 0.15;
|
||||
|
||||
this._updateParticles(time);
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
@@ -120,6 +222,11 @@ class Agent {
|
||||
this.glow.material.dispose();
|
||||
this.sprite.material.map.dispose();
|
||||
this.sprite.material.dispose();
|
||||
if (this._pMesh) {
|
||||
this._pMesh.geometry.dispose();
|
||||
this._pMesh.material.dispose();
|
||||
this._pMesh = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user