forked from Rockachopa/the-matrix
Compare commits
1 Commits
claude/iss
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21caa2ad5e |
124
js/agents.js
124
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() {
|
||||
@@ -90,27 +190,22 @@ class Agent {
|
||||
this.group.add(this.sprite);
|
||||
}
|
||||
|
||||
update(time, nightFactor = 0) {
|
||||
update(time) {
|
||||
const pulse = Math.sin(time * 0.002 + this.pulsePhase);
|
||||
const active = this.state === 'active';
|
||||
|
||||
// Agents glow brighter at night
|
||||
const nightBoost = nightFactor * 0.45;
|
||||
const intensity = active ? 0.6 + pulse * 0.4 + nightBoost : 0.2 + pulse * 0.1 + nightBoost;
|
||||
const intensity = active ? 0.6 + pulse * 0.4 : 0.2 + pulse * 0.1;
|
||||
this.core.material.emissiveIntensity = intensity;
|
||||
|
||||
const lightBase = active ? 2 + pulse : 0.8 + pulse * 0.3;
|
||||
this.light.intensity = lightBase * (1 + nightFactor * 0.6);
|
||||
this.light.intensity = active ? 2 + pulse : 0.8 + pulse * 0.3;
|
||||
|
||||
const scale = active ? 1 + pulse * 0.08 : 1 + pulse * 0.03;
|
||||
this.core.scale.setScalar(scale);
|
||||
this.ring.rotation.y += active ? 0.03 : 0.008;
|
||||
this.ring.material.opacity = 0.3 + pulse * 0.2;
|
||||
|
||||
// Glow shell more visible at night
|
||||
this.glow.material.opacity = 0.04 + nightFactor * 0.12 + (active ? pulse * 0.04 : 0);
|
||||
|
||||
this.group.position.y = this.position.y + Math.sin(time * 0.001 + this.pulsePhase) * 0.15;
|
||||
|
||||
this._updateParticles(time);
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
@@ -127,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,8 +268,8 @@ function buildConnectionLines() {
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAgents(time, nightFactor = 0) {
|
||||
agents.forEach(agent => agent.update(time, nightFactor));
|
||||
export function updateAgents(time) {
|
||||
agents.forEach(agent => agent.update(time));
|
||||
}
|
||||
|
||||
export function getAgentCount() {
|
||||
|
||||
127
js/daynight.js
127
js/daynight.js
@@ -1,127 +0,0 @@
|
||||
/**
|
||||
* Day/night cycle based on real UTC time.
|
||||
*
|
||||
* Sunrise ~06:00 UTC, Sunset ~18:00 UTC.
|
||||
* Drives: sun directional light, ambient light, fog color, star visibility.
|
||||
* Returns a nightFactor (0=day, 1=night) for consumers (e.g. agent glow).
|
||||
*/
|
||||
import * as THREE from 'three';
|
||||
|
||||
let sunLight = null;
|
||||
let ambientRef = null;
|
||||
let sceneRef = null;
|
||||
let starMatRef = null;
|
||||
|
||||
/** Call once during world init. */
|
||||
export function initDayNight(scene, ambientLight) {
|
||||
sceneRef = scene;
|
||||
ambientRef = ambientLight;
|
||||
|
||||
sunLight = new THREE.DirectionalLight(0xffffff, 0);
|
||||
sunLight.position.set(0, 80, 0);
|
||||
scene.add(sunLight);
|
||||
}
|
||||
|
||||
/** Called by effects.js after it creates the star PointsMaterial. */
|
||||
export function setStarMaterial(mat) {
|
||||
starMatRef = mat;
|
||||
}
|
||||
|
||||
/** Returns seconds elapsed since UTC midnight. */
|
||||
function utcSeconds() {
|
||||
const n = new Date();
|
||||
return n.getUTCHours() * 3600 + n.getUTCMinutes() * 60 + n.getUTCSeconds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sun elevation angle: 1 at UTC noon, -1 at UTC midnight.
|
||||
* Zero-crossings at 06:00 (dawn) and 18:00 (dusk).
|
||||
*/
|
||||
function sunElevation() {
|
||||
const frac = utcSeconds() / 86400; // 0–1
|
||||
return Math.sin(2 * Math.PI * (frac - 0.25));
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooth step from a to b.
|
||||
* @param {number} edge0
|
||||
* @param {number} edge1
|
||||
* @param {number} x
|
||||
*/
|
||||
function smoothstep(edge0, edge1, x) {
|
||||
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all day/night-driven scene properties.
|
||||
* Called every animation frame.
|
||||
* @returns {number} nightFactor — 0 (full day) to 1 (full night)
|
||||
*/
|
||||
export function updateDayNight() {
|
||||
const elev = sunElevation();
|
||||
|
||||
// dayFactor: 1 at full day (elev > 0.15), 0 at full night (elev < -0.15)
|
||||
const dayFactor = smoothstep(-0.15, 0.15, elev);
|
||||
const nightFactor = 1 - dayFactor;
|
||||
|
||||
// ── Sun directional light ─────────────────────────────────────────────────
|
||||
if (sunLight) {
|
||||
// Arc: east (positive X) at dawn, overhead at noon, west at dusk
|
||||
const frac = utcSeconds() / 86400;
|
||||
const angle = 2 * Math.PI * (frac - 0.25);
|
||||
const sx = -Math.cos(angle) * 60;
|
||||
const sy = Math.sin(angle) * 80;
|
||||
const sz = -30;
|
||||
sunLight.position.set(sx, Math.max(sy, 0.1), sz);
|
||||
|
||||
// Color: warm orange near horizon, muted matrix-green at zenith
|
||||
const horizonBlend = 1 - Math.min(1, Math.abs(elev) / 0.4);
|
||||
const r = (0.3 + horizonBlend * 0.7) * dayFactor;
|
||||
const g = (0.6 - horizonBlend * 0.2) * dayFactor;
|
||||
const b = (0.3 - horizonBlend * 0.25) * dayFactor;
|
||||
sunLight.color.setRGB(r, g, b);
|
||||
sunLight.intensity = dayFactor * 1.2;
|
||||
}
|
||||
|
||||
// ── Ambient light ─────────────────────────────────────────────────────────
|
||||
if (ambientRef) {
|
||||
// Night: very dark greenish. Day: slightly brighter, cyan tint.
|
||||
ambientRef.intensity = 0.35 + dayFactor * 0.35;
|
||||
const ar = dayFactor * 0.04;
|
||||
const ag = 0.07 + dayFactor * 0.05;
|
||||
const ab = dayFactor * 0.06;
|
||||
ambientRef.color.setRGB(ar, ag, ab);
|
||||
}
|
||||
|
||||
// ── Fog color ─────────────────────────────────────────────────────────────
|
||||
if (sceneRef && sceneRef.fog) {
|
||||
const fr = dayFactor * 0.008;
|
||||
const fg = dayFactor * 0.016;
|
||||
const fb = dayFactor * 0.010;
|
||||
sceneRef.fog.color.setRGB(fr, fg, fb);
|
||||
}
|
||||
|
||||
// ── Stars ─────────────────────────────────────────────────────────────────
|
||||
if (starMatRef) {
|
||||
starMatRef.opacity = 0.05 + nightFactor * 0.80;
|
||||
// Night: bright green. Day: nearly invisible dark green.
|
||||
const sg = 0.05 + nightFactor * 0.55;
|
||||
starMatRef.color.setRGB(0, sg, 0);
|
||||
starMatRef.needsUpdate = true;
|
||||
}
|
||||
|
||||
return nightFactor;
|
||||
}
|
||||
|
||||
/** Remove sun light from scene and clear module refs. */
|
||||
export function disposeDayNight() {
|
||||
if (sunLight && sceneRef) {
|
||||
sceneRef.remove(sunLight);
|
||||
sunLight.dispose();
|
||||
}
|
||||
sunLight = null;
|
||||
ambientRef = null;
|
||||
sceneRef = null;
|
||||
starMatRef = null;
|
||||
}
|
||||
8
js/effects.js
vendored
8
js/effects.js
vendored
@@ -1,6 +1,5 @@
|
||||
import * as THREE from 'three';
|
||||
import { getQualityTier } from './quality.js';
|
||||
import { setStarMaterial } from './daynight.js';
|
||||
|
||||
let rainParticles;
|
||||
let rainPositions;
|
||||
@@ -71,14 +70,11 @@ function initStarfield(scene, tier) {
|
||||
color: 0x003300,
|
||||
size: 0.08,
|
||||
transparent: true,
|
||||
opacity: 0.05, // daynight.js will drive this each frame
|
||||
opacity: 0.5,
|
||||
});
|
||||
|
||||
const stars = new THREE.Points(geo, mat);
|
||||
scene.add(stars);
|
||||
|
||||
// Register with day/night system so it can control star visibility
|
||||
setStarMaterial(mat);
|
||||
}
|
||||
|
||||
export function updateEffects(_time) {
|
||||
@@ -118,6 +114,4 @@ export function disposeEffects() {
|
||||
rainVelocities = null;
|
||||
rainCount = 0;
|
||||
frameCounter = 0;
|
||||
// Clear star material ref in daynight (daynight.js handles its own cleanup)
|
||||
setStarMaterial(null);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
disposeAgents, getAgentStates, applyAgentStates,
|
||||
} from './agents.js';
|
||||
import { initEffects, updateEffects, disposeEffects } from './effects.js';
|
||||
import { updateDayNight } from './daynight.js';
|
||||
import { initUI, updateUI } from './ui.js';
|
||||
import { initInteraction, updateControls, disposeInteraction } from './interaction.js';
|
||||
import { initWebSocket, getConnectionState, getJobCount } from './websocket.js';
|
||||
@@ -74,8 +73,7 @@ function buildWorld(firstInit, stateSnapshot) {
|
||||
|
||||
updateControls();
|
||||
updateEffects(now);
|
||||
const nightFactor = updateDayNight();
|
||||
updateAgents(now, nightFactor);
|
||||
updateAgents(now);
|
||||
updateUI({
|
||||
fps: currentFps,
|
||||
agentCount: getAgentCount(),
|
||||
|
||||
10
js/world.js
10
js/world.js
@@ -1,6 +1,5 @@
|
||||
import * as THREE from 'three';
|
||||
import { getMaxPixelRatio, getQualityTier } from './quality.js';
|
||||
import { initDayNight, disposeDayNight } from './daynight.js';
|
||||
|
||||
let scene, camera, renderer;
|
||||
const _worldObjects = [];
|
||||
@@ -31,16 +30,14 @@ export function initWorld(existingCanvas) {
|
||||
document.body.prepend(renderer.domElement);
|
||||
}
|
||||
|
||||
const { ambient } = addLights(scene);
|
||||
addLights(scene);
|
||||
addGrid(scene, tier);
|
||||
|
||||
initDayNight(scene, ambient);
|
||||
|
||||
return { scene, camera, renderer };
|
||||
}
|
||||
|
||||
function addLights(scene) {
|
||||
const ambient = new THREE.AmbientLight(0x001a00, 0.35);
|
||||
const ambient = new THREE.AmbientLight(0x001a00, 0.6);
|
||||
scene.add(ambient);
|
||||
|
||||
const point = new THREE.PointLight(0x00ff41, 2, 80);
|
||||
@@ -50,8 +47,6 @@ function addLights(scene) {
|
||||
const fill = new THREE.DirectionalLight(0x003300, 0.4);
|
||||
fill.position.set(-10, 10, 10);
|
||||
scene.add(fill);
|
||||
|
||||
return { ambient };
|
||||
}
|
||||
|
||||
function addGrid(scene, tier) {
|
||||
@@ -90,7 +85,6 @@ export function disposeWorld(disposeRenderer, _scene) {
|
||||
}
|
||||
}
|
||||
_worldObjects.length = 0;
|
||||
disposeDayNight();
|
||||
disposeRenderer.dispose();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user