Files
the-matrix/js/agents.js
Perplexity Computer fdfae19956 feat: The Matrix — Sovereign Agent World
3D visualization for AI agent swarms built with Three.js.
Matrix green/noir cyberpunk aesthetic.

- 4 agents: Timmy (orchestrator), Forge (builder), Seer (planner), Echo (comms)
- Central core pillar, animated green grid, digital rain
- Agent info panels, chat, task list, memory views
- WebSocket protocol for real-time state updates
- iPad-ready: touch controls, add-to-homescreen
- Post-processing: bloom, scanlines, vignette
- No build step — pure ES modules via esm.sh CDN

Created with Perplexity Computer
2026-03-18 18:32:47 -04:00

525 lines
16 KiB
JavaScript

// ===== Agents: Geometry, animations, labels, task objects =====
import * as THREE from 'three';
import { AGENT_DEFS, AGENT_IDS } from './websocket.js';
const AGENT_POSITIONS = {
timmy: { x: 0, z: -14 }, // North
forge: { x: 14, z: 0 }, // East
seer: { x: 0, z: 14 }, // South
echo: { x: -14, z: 0 }, // West
};
const AGENT_COLORS = {
timmy: 0x00ff41,
forge: 0xff8c00,
seer: 0x9d4edd,
echo: 0x00d4ff,
};
// Create text sprite
function createLabel(text, color) {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 64;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 256, 64);
ctx.font = '600 28px "JetBrains Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = 12;
ctx.fillText(text, 128, 32);
ctx.shadowBlur = 0;
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
const mat = new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthWrite: false,
});
const sprite = new THREE.Sprite(mat);
sprite.scale.set(4, 1, 1);
return sprite;
}
// Create hex platform
function createHexPlatform(color) {
const shape = new THREE.Shape();
const r = 3.5;
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i - Math.PI / 6;
const x = r * Math.cos(angle);
const y = r * Math.sin(angle);
if (i === 0) shape.moveTo(x, y);
else shape.lineTo(x, y);
}
shape.closePath();
const extrudeSettings = { depth: 0.2, bevelEnabled: false };
const geom = new THREE.ExtrudeGeometry(shape, extrudeSettings);
geom.rotateX(-Math.PI / 2);
const mat = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.08,
});
const mesh = new THREE.Mesh(geom, mat);
// Hex border
const edgePoints = [];
for (let i = 0; i <= 6; i++) {
const angle = (Math.PI / 3) * (i % 6) - Math.PI / 6;
edgePoints.push(new THREE.Vector3(r * Math.cos(angle), 0.22, r * Math.sin(angle)));
}
const lineGeom = new THREE.BufferGeometry().setFromPoints(edgePoints);
const lineMat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.55 });
const line = new THREE.Line(lineGeom, lineMat);
const group = new THREE.Group();
group.add(mesh);
group.add(line);
return group;
}
// ===== TIMMY - Wireframe humanoid =====
function createTimmy() {
const group = new THREE.Group();
const color = AGENT_COLORS.timmy;
const mat = new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.8 });
// Head
const head = new THREE.Mesh(new THREE.IcosahedronGeometry(0.45, 1), mat);
head.position.y = 3.2;
group.add(head);
// Torso
const torso = new THREE.Mesh(new THREE.BoxGeometry(0.9, 1.4, 0.5), mat);
torso.position.y = 2.0;
group.add(torso);
// Arms
[-0.65, 0.65].forEach(x => {
const arm = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.2, 0.2), mat);
arm.position.set(x, 2.0, 0);
group.add(arm);
});
// Legs
[-0.25, 0.25].forEach(x => {
const leg = new THREE.Mesh(new THREE.BoxGeometry(0.25, 1.2, 0.25), mat);
leg.position.set(x, 0.7, 0);
group.add(leg);
});
// Glow point
const glow = new THREE.PointLight(color, 1, 8);
glow.position.y = 2;
group.add(glow);
return group;
}
// ===== FORGE - Anvil/hammer geometric =====
function createForge() {
const group = new THREE.Group();
const color = AGENT_COLORS.forge;
const mat = new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.6 });
const wireMat = new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.5 });
// Base cube (anvil)
const base = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.8, 1.0), mat);
base.position.y = 1.2;
group.add(base);
// Top pyramid (hammer head)
const pyramid = new THREE.Mesh(new THREE.ConeGeometry(0.8, 1.2, 4), wireMat);
pyramid.position.y = 2.6;
pyramid.rotation.y = Math.PI / 4;
group.add(pyramid);
// Floating cubes
for (let i = 0; i < 3; i++) {
const cube = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, 0.3), wireMat.clone());
cube.position.set(
Math.cos(i * Math.PI * 2 / 3) * 1.5,
2.5 + i * 0.3,
Math.sin(i * Math.PI * 2 / 3) * 1.5
);
cube.userData.orbitIndex = i;
group.add(cube);
}
const glow = new THREE.PointLight(color, 1, 8);
glow.position.y = 2;
group.add(glow);
return group;
}
// ===== SEER - Crystal ball / icosahedron =====
function createSeer() {
const group = new THREE.Group();
const color = AGENT_COLORS.seer;
const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.6 });
const wireMat = new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.8 });
// Main crystal (icosahedron)
const crystal = new THREE.Mesh(new THREE.IcosahedronGeometry(1.0, 0), wireMat);
crystal.position.y = 2.5;
crystal.userData.isCrystal = true;
group.add(crystal);
// Inner glow sphere
const inner = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 16), mat);
inner.position.y = 2.5;
group.add(inner);
// Orbiting small spheres
for (let i = 0; i < 5; i++) {
const orb = new THREE.Mesh(new THREE.SphereGeometry(0.12, 8, 8), mat.clone());
orb.userData.orbitIndex = i;
orb.userData.orbitRadius = 1.8;
orb.position.y = 2.5;
group.add(orb);
}
const glow = new THREE.PointLight(color, 1, 8);
glow.position.y = 2.5;
group.add(glow);
return group;
}
// ===== ECHO - Concentric pulse rings =====
function createEcho() {
const group = new THREE.Group();
const color = AGENT_COLORS.echo;
// Central sphere
const coreMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.7 });
const core = new THREE.Mesh(new THREE.SphereGeometry(0.4, 16, 16), coreMat);
core.position.y = 2.2;
group.add(core);
// Concentric rings
for (let i = 0; i < 4; i++) {
const ring = new THREE.Mesh(
new THREE.TorusGeometry(1.0 + i * 0.6, 0.04, 8, 48),
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.65 - i * 0.1 })
);
ring.position.y = 2.2;
ring.rotation.x = Math.PI / 2;
ring.userData.ringIndex = i;
ring.userData.baseScale = 1;
group.add(ring);
}
const glow = new THREE.PointLight(color, 1, 8);
glow.position.y = 2.2;
group.add(glow);
return group;
}
const AGENT_CREATORS = { timmy: createTimmy, forge: createForge, seer: createSeer, echo: createEcho };
// ===== Task Objects =====
const TASK_STATUS_COLORS = {
pending: 0xffffff,
in_progress: 0xff8c00,
completed: 0x00ff41,
failed: 0xff3333,
};
function createTaskObject(status) {
const geoms = [
new THREE.IcosahedronGeometry(0.25, 0),
new THREE.OctahedronGeometry(0.25, 0),
new THREE.TetrahedronGeometry(0.3, 0),
];
const geom = geoms[Math.floor(Math.random() * geoms.length)];
const color = TASK_STATUS_COLORS[status] || 0xffffff;
const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.7, wireframe: true });
const mesh = new THREE.Mesh(geom, mat);
return mesh;
}
// ===== Connection Lines =====
function createConnectionLine(scene, fromPos, toPos, color = 0x00ff41) {
const points = [
new THREE.Vector3(fromPos.x, 2, fromPos.z),
new THREE.Vector3(toPos.x, 2, toPos.z),
];
const geom = new THREE.BufferGeometry().setFromPoints(points);
const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.3 });
const line = new THREE.Line(geom, mat);
line.userData.isConnection = true;
scene.add(line);
return line;
}
// ===== Main Agents System =====
export function createAgents(scene) {
const agentGroups = {};
const agentPlatforms = {};
const taskObjects = {};
const connectionLines = {};
const agentStates = {};
// Create each agent
AGENT_IDS.forEach(id => {
const pos = AGENT_POSITIONS[id];
const color = AGENT_COLORS[id];
const colorHex = '#' + color.toString(16).padStart(6, '0');
// Platform
const platform = createHexPlatform(color);
platform.position.set(pos.x, 0, pos.z);
scene.add(platform);
agentPlatforms[id] = platform;
// Agent geometry
const agentGeo = AGENT_CREATORS[id]();
agentGeo.position.set(pos.x, 0, pos.z);
agentGeo.name = `agent_${id}`;
agentGeo.userData.agentId = id;
agentGeo.userData.isAgent = true;
scene.add(agentGeo);
agentGroups[id] = agentGeo;
// Label
const label = createLabel(AGENT_DEFS[id].name.toUpperCase(), colorHex);
label.position.set(pos.x, 5, pos.z);
scene.add(label);
// State
agentStates[id] = {
state: 'idle',
glowIntensity: 0.5,
targetGlow: 0.5,
bobOffset: Math.random() * Math.PI * 2,
};
});
return {
agentGroups,
agentPlatforms,
taskObjects,
connectionLines,
agentStates,
// Get the raycasting targets
getInteractables() {
const targets = [];
Object.values(agentGroups).forEach(group => {
group.traverse(child => {
if (child.isMesh) {
child.userData.agentId = group.userData.agentId;
child.userData.isAgent = true;
targets.push(child);
}
});
});
return targets;
},
setAgentState(agentId, state, glowIntensity) {
if (!agentStates[agentId]) return;
agentStates[agentId].state = state;
agentStates[agentId].targetGlow = glowIntensity;
},
highlightAgent(agentId, highlight) {
const group = agentGroups[agentId];
if (!group) return;
group.traverse(child => {
if (child.isMesh && child.material) {
if (highlight) {
child.material._origOpacity = child.material._origOpacity ?? child.material.opacity;
child.material.opacity = Math.min(1, child.material._origOpacity + 0.3);
} else if (child.material._origOpacity !== undefined) {
child.material.opacity = child.material._origOpacity;
}
}
});
},
updateConnection(agentA, agentB, active) {
const key = [agentA, agentB].sort().join('-');
if (active) {
if (!connectionLines[key]) {
const posA = AGENT_POSITIONS[agentA];
const posB = AGENT_POSITIONS[agentB];
if (posA && posB) {
connectionLines[key] = createConnectionLine(scene, posA, posB);
}
}
} else {
if (connectionLines[key]) {
connectionLines[key].geometry.dispose();
connectionLines[key].material.dispose();
scene.remove(connectionLines[key]);
delete connectionLines[key];
}
}
},
addTaskObject(taskId, agentId, status) {
if (taskObjects[taskId]) return;
const pos = AGENT_POSITIONS[agentId];
if (!pos) return;
const obj = createTaskObject(status);
obj.userData.taskId = taskId;
obj.userData.agentId = agentId;
obj.userData.orbitAngle = Math.random() * Math.PI * 2;
obj.userData.orbitRadius = 4.5 + Math.random() * 1.5;
obj.userData.orbitSpeed = 0.2 + Math.random() * 0.3;
obj.userData.orbitY = 1.5 + Math.random() * 2;
obj.position.set(pos.x, obj.userData.orbitY, pos.z);
scene.add(obj);
taskObjects[taskId] = obj;
},
updateTaskObject(taskId, status) {
const obj = taskObjects[taskId];
if (!obj) return;
const color = TASK_STATUS_COLORS[status] || 0xffffff;
obj.material.color.setHex(color);
},
removeTaskObject(taskId) {
const obj = taskObjects[taskId];
if (!obj) return;
obj.geometry.dispose();
obj.material.dispose();
scene.remove(obj);
delete taskObjects[taskId];
},
update(time, delta) {
// Animate agents
AGENT_IDS.forEach(id => {
const group = agentGroups[id];
const state = agentStates[id];
const pos = AGENT_POSITIONS[id];
// Floating bob
const bobSpeed = state.state === 'working' ? 2.5 : 1.2;
const bobAmp = state.state === 'working' ? 0.3 : 0.15;
const bob = Math.sin(time * bobSpeed + state.bobOffset) * bobAmp;
group.position.y = bob;
// Slow rotation
const rotSpeed = state.state === 'working' ? 0.4 : 0.15;
group.rotation.y = time * rotSpeed;
// Glow intensity lerp
state.glowIntensity += (state.targetGlow - state.glowIntensity) * delta * 2;
group.traverse(child => {
if (child.isPointLight) {
child.intensity = state.glowIntensity * 2;
}
});
// Agent-specific animations
if (id === 'forge') {
group.children.forEach(child => {
if (child.userData.orbitIndex !== undefined) {
const i = child.userData.orbitIndex;
const angle = time * 0.8 + i * (Math.PI * 2 / 3);
child.position.x = pos.x + Math.cos(angle) * 1.5;
child.position.z = pos.z + Math.sin(angle) * 1.5;
child.position.y = 2.5 + Math.sin(time * 1.5 + i) * 0.3;
child.rotation.x = time;
child.rotation.z = time * 0.7;
}
});
}
if (id === 'seer') {
group.children.forEach(child => {
if (child.userData.isCrystal) {
child.rotation.x = time * 0.5;
child.rotation.z = time * 0.3;
}
if (child.userData.orbitIndex !== undefined) {
const i = child.userData.orbitIndex;
const angle = time * 0.6 + i * (Math.PI * 2 / 5);
const r = child.userData.orbitRadius;
child.position.x = pos.x + Math.cos(angle) * r;
child.position.z = pos.z + Math.sin(angle) * r;
child.position.y = 2.5 + Math.sin(time + i * 0.5) * 0.5;
}
});
}
if (id === 'echo') {
group.children.forEach(child => {
if (child.userData.ringIndex !== undefined) {
const i = child.userData.ringIndex;
const pulse = Math.sin(time * 2 - i * 0.5) * 0.5 + 0.5;
const scale = 1 + pulse * 0.3;
child.scale.set(scale, scale, scale);
child.material.opacity = (0.6 - i * 0.12) * (0.5 + pulse * 0.5);
child.rotation.z = time * 0.3 * (i % 2 === 0 ? 1 : -1);
}
});
}
});
// Animate task objects
Object.values(taskObjects).forEach(obj => {
const pos = AGENT_POSITIONS[obj.userData.agentId];
if (!pos) return;
obj.userData.orbitAngle += obj.userData.orbitSpeed * delta;
const angle = obj.userData.orbitAngle;
const r = obj.userData.orbitRadius;
obj.position.x = pos.x + Math.cos(angle) * r;
obj.position.z = pos.z + Math.sin(angle) * r;
obj.position.y = obj.userData.orbitY + Math.sin(time * 1.5 + angle) * 0.3;
obj.rotation.x = time * 0.8;
obj.rotation.z = time * 0.5;
});
// Animate connection lines (pulse opacity)
Object.values(connectionLines).forEach(line => {
line.material.opacity = 0.15 + Math.sin(time * 3) * 0.15;
});
},
dispose() {
Object.values(agentGroups).forEach(group => {
group.traverse(obj => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) obj.material.dispose();
});
scene.remove(group);
});
Object.values(agentPlatforms).forEach(p => {
p.traverse(obj => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) obj.material.dispose();
});
scene.remove(p);
});
Object.values(taskObjects).forEach(obj => {
obj.geometry.dispose();
obj.material.dispose();
scene.remove(obj);
});
Object.values(connectionLines).forEach(line => {
line.geometry.dispose();
line.material.dispose();
scene.remove(line);
});
}
};
}
export { AGENT_POSITIONS, AGENT_COLORS };