forked from Rockachopa/the-matrix
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
284 lines
7.7 KiB
JavaScript
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();
|
|
}
|
|
};
|
|
}
|