Files
the-matrix/js/effects.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

284 lines
7.7 KiB
JavaScript

// ===== Effects: Digital rain, particles, post-processing, scanlines =====
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
// ===== Vignette + Scanline Shader =====
const VignetteScanlineShader = {
uniforms: {
tDiffuse: { value: null },
uTime: { value: 0 },
uVignetteIntensity: { value: 0.4 },
uScanlineIntensity: { value: 0.06 },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float uTime;
uniform float uVignetteIntensity;
uniform float uScanlineIntensity;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
// Scanlines
float scanline = sin(vUv.y * 800.0 + uTime * 2.0) * 0.5 + 0.5;
color.rgb -= scanline * uScanlineIntensity;
// Vignette
vec2 uv = vUv * 2.0 - 1.0;
float vignette = 1.0 - dot(uv, uv) * uVignetteIntensity;
color.rgb *= vignette;
gl_FragColor = color;
}
`,
};
// ===== Digital Rain Shader (on a cylinder) =====
const rainVertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const rainFragmentShader = `
uniform float uTime;
uniform float uDensity;
varying vec2 vUv;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float hash2(vec2 p) {
return fract(sin(dot(p, vec2(269.5, 183.3))) * 43758.5453);
}
void main() {
vec2 uv = vUv;
float cols = uDensity;
float rows = 60.0;
// Column index
float col = floor(uv.x * cols);
float colFrac = fract(uv.x * cols);
// Each column properties
float speed = 0.15 + hash(vec2(col, 0.0)) * 0.45;
float offset = hash(vec2(col, 1.0)) * 200.0;
float length = 8.0 + hash(vec2(col, 2.0)) * 18.0;
// Scrolling Y
float scrollY = uv.y * rows + uTime * speed * rows + offset;
float rowIdx = floor(scrollY);
float rowFrac = fract(scrollY);
// Character glyph simulation - small rectangle within cell
float charMarginX = 0.15;
float charMarginY = 0.12;
float inCharX = step(charMarginX, colFrac) * step(charMarginX, 1.0 - colFrac);
float inCharY = step(charMarginY, rowFrac) * step(charMarginY, 1.0 - rowFrac);
float inChar = inCharX * inCharY;
// Random per-cell character presence and flicker
float charSeed = hash(vec2(col, rowIdx));
float charPresent = step(0.3, charSeed);
// Flicker: some cells change
float flicker = hash2(vec2(col, rowIdx + floor(uTime * 3.0)));
charPresent *= step(0.15, flicker);
// "Glyph" pattern within cell using sub-hash
float glyphDetail = hash(vec2(col * 13.0 + floor(colFrac * 3.0), rowIdx * 7.0 + floor(rowFrac * 4.0) + floor(uTime * 2.0)));
float glyphMask = step(0.35, glyphDetail);
// Trail: head of each stream is brightest, fades behind
float streamPhase = fract(uTime * speed * 0.5 + hash(vec2(col, 3.0)));
float headPos = streamPhase * (rows + length);
float distFromHead = mod(scrollY - headPos, rows + length);
float inStream = smoothstep(length, 0.0, distFromHead);
// Leading character glow
float isHead = smoothstep(2.0, 0.0, distFromHead);
// Combine
float alpha = inChar * charPresent * glyphMask * inStream * 0.5;
alpha += isHead * inChar * 0.6;
// Column brightness variation
float colBrightness = 0.6 + hash(vec2(col, 5.0)) * 0.4;
alpha *= colBrightness;
// Vertical fade at top/bottom
float vertFade = smoothstep(0.0, 0.1, uv.y) * smoothstep(1.0, 0.85, uv.y);
alpha *= vertFade;
// Color
vec3 color = mix(vec3(0.0, 0.35, 0.02), vec3(0.0, 1.0, 0.25), isHead * 0.7 + inStream * 0.3);
gl_FragColor = vec4(color, alpha);
}
`;
// ===== Create Digital Rain Backdrop =====
function createDigitalRain(scene) {
const radius = 58;
const height = 50;
const segments = 64;
const geom = new THREE.CylinderGeometry(radius, radius, height, segments, 1, true);
const mat = new THREE.ShaderMaterial({
vertexShader: rainVertexShader,
fragmentShader: rainFragmentShader,
uniforms: {
uTime: { value: 0 },
uDensity: { value: 80 },
},
transparent: true,
side: THREE.BackSide,
depthWrite: false,
});
const mesh = new THREE.Mesh(geom, mat);
mesh.position.y = height / 2 - 8;
mesh.renderOrder = -2;
scene.add(mesh);
return {
mesh,
update(time) {
mat.uniforms.uTime.value = time;
},
dispose() {
geom.dispose();
mat.dispose();
scene.remove(mesh);
}
};
}
// ===== Floating Particles =====
function createParticles(scene) {
const count = 200;
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
const sizes = new Float32Array(count);
const green = new THREE.Color(0x00ff41);
const dimGreen = new THREE.Color(0x003b00);
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const r = 5 + Math.random() * 45;
positions[i * 3] = Math.cos(angle) * r;
positions[i * 3 + 1] = Math.random() * 25;
positions[i * 3 + 2] = Math.sin(angle) * r;
const c = Math.random() > 0.7 ? green : dimGreen;
colors[i * 3] = c.r;
colors[i * 3 + 1] = c.g;
colors[i * 3 + 2] = c.b;
sizes[i] = 0.5 + Math.random() * 1.5;
}
const geom = new THREE.BufferGeometry();
geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geom.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geom.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const mat = new THREE.PointsMaterial({
size: 0.15,
vertexColors: true,
transparent: true,
opacity: 0.5,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
const points = new THREE.Points(geom, mat);
scene.add(points);
return {
points,
update(time) {
const pos = geom.attributes.position.array;
for (let i = 0; i < count; i++) {
pos[i * 3 + 1] += 0.005 + Math.sin(time + i) * 0.002;
if (pos[i * 3 + 1] > 25) pos[i * 3 + 1] = 0;
}
geom.attributes.position.needsUpdate = true;
},
dispose() {
geom.dispose();
mat.dispose();
scene.remove(points);
}
};
}
// ===== Setup Post-Processing =====
export function setupEffects(renderer, scene, camera) {
const composer = new EffectComposer(renderer);
// Render pass
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// Bloom — make green elements GLOW
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.0, // strength
0.4, // radius
0.5 // threshold
);
composer.addPass(bloomPass);
// Vignette + scanlines
const vignettePass = new ShaderPass(VignetteScanlineShader);
composer.addPass(vignettePass);
// Digital rain
const rain = createDigitalRain(scene);
// Particles
const particles = createParticles(scene);
return {
composer,
bloomPass,
vignettePass,
rain,
particles,
update(time, delta) {
vignettePass.uniforms.uTime.value = time;
rain.update(time);
particles.update(time);
},
resize(width, height) {
composer.setSize(width, height);
bloomPass.resolution.set(width, height);
},
dispose() {
composer.dispose();
rain.dispose();
particles.dispose();
}
};
}