[claude] Lightning arcs between floating crystals during high activity (#257) (#338)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled

This commit was merged in pull request #338.
This commit is contained in:
2026-03-24 05:06:21 +00:00
parent c24b69359f
commit fcb198f55d

145
app.js
View File

@@ -1073,6 +1073,134 @@ function startWarp() {
warpPass.uniforms['distortionStrength'].value = 0.0;
}
// === FLOATING CRYSTALS & LIGHTNING ARCS ===
// Crystals float above the platform. When zone activity is high, lightning arcs jump between them.
const CRYSTAL_COUNT = 5;
const CRYSTAL_BASE_POSITIONS = [
new THREE.Vector3(-4.5, 3.2, -3.8),
new THREE.Vector3( 4.8, 2.8, -4.0),
new THREE.Vector3(-5.5, 4.0, 1.5),
new THREE.Vector3( 5.2, 3.5, 2.0),
new THREE.Vector3( 0.0, 5.0, -5.5),
];
// Colors aligned to agent zones: Claude, Timmy, Kimi, Perplexity, center
const CRYSTAL_COLORS = [0xff6440, 0x40a0ff, 0x40ff8c, 0xc840ff, 0xffd700];
const crystalGroup = new THREE.Group();
scene.add(crystalGroup);
/**
* @type {Array<{mesh: THREE.Mesh, light: THREE.PointLight, basePos: THREE.Vector3, floatPhase: number}>}
*/
const crystals = [];
for (let i = 0; i < CRYSTAL_COUNT; i++) {
const geo = new THREE.OctahedronGeometry(0.35, 0);
const color = CRYSTAL_COLORS[i];
const mat = new THREE.MeshStandardMaterial({
color,
emissive: new THREE.Color(color).multiplyScalar(0.6),
roughness: 0.05,
metalness: 0.3,
transparent: true,
opacity: 0.88,
});
const mesh = new THREE.Mesh(geo, mat);
const basePos = CRYSTAL_BASE_POSITIONS[i].clone();
mesh.position.copy(basePos);
mesh.userData.zoomLabel = 'Crystal';
crystalGroup.add(mesh);
const light = new THREE.PointLight(color, 0.3, 6);
light.position.copy(basePos);
crystalGroup.add(light);
crystals.push({ mesh, light, basePos, floatPhase: (i / CRYSTAL_COUNT) * Math.PI * 2 });
}
// ---- Lightning arc pool ----
const LIGHTNING_POOL_SIZE = 6;
const LIGHTNING_SEGMENTS = 8;
const LIGHTNING_REFRESH_MS = 130;
let lastLightningRefreshTime = 0;
/** @type {Array<THREE.Line>} */
const lightningArcs = [];
for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const positions = new Float32Array((LIGHTNING_SEGMENTS + 1) * 3);
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.LineBasicMaterial({
color: 0x88ccff,
transparent: true,
opacity: 0.0,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const arc = new THREE.Line(geo, mat);
scene.add(arc);
lightningArcs.push(arc);
}
/**
* Builds a jagged lightning path between two points.
* @param {THREE.Vector3} start
* @param {THREE.Vector3} end
* @param {number} jagAmount
* @returns {Float32Array}
*/
function buildLightningPath(start, end, jagAmount) {
const out = new Float32Array((LIGHTNING_SEGMENTS + 1) * 3);
for (let s = 0; s <= LIGHTNING_SEGMENTS; s++) {
const t = s / LIGHTNING_SEGMENTS;
const x = start.x + (end.x - start.x) * t + (s > 0 && s < LIGHTNING_SEGMENTS ? (Math.random() - 0.5) * jagAmount : 0);
const y = start.y + (end.y - start.y) * t + (s > 0 && s < LIGHTNING_SEGMENTS ? (Math.random() - 0.5) * jagAmount : 0);
const z = start.z + (end.z - start.z) * t + (s > 0 && s < LIGHTNING_SEGMENTS ? (Math.random() - 0.5) * jagAmount : 0);
out[s * 3] = x; out[s * 3 + 1] = y; out[s * 3 + 2] = z;
}
return out;
}
/**
* Returns mean activity [0..1] across all agent zones.
*/
function totalActivity() {
const vals = Object.values(zoneIntensity);
return vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
}
/**
* Refreshes lightning arcs based on current activity level.
*/
function updateLightningArcs() {
const activity = totalActivity();
const activeCount = Math.round(activity * LIGHTNING_POOL_SIZE);
for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const arc = lightningArcs[i];
if (i >= activeCount) {
arc.material.opacity = 0;
continue;
}
const a = Math.floor(Math.random() * CRYSTAL_COUNT);
let b = Math.floor(Math.random() * (CRYSTAL_COUNT - 1));
if (b >= a) b++;
const jagAmount = 0.45 + activity * 0.85;
const path = buildLightningPath(crystals[a].mesh.position, crystals[b].mesh.position, jagAmount);
const attr = arc.geometry.attributes.position;
attr.array.set(path);
attr.needsUpdate = true;
arc.material.opacity = (0.35 + Math.random() * 0.55) * Math.min(activity * 1.5, 1.0);
arc.material.color.setHex(CRYSTAL_COLORS[Math.floor(Math.random() * CRYSTAL_COLORS.length)]);
}
}
// === ANIMATION LOOP ===
const clock = new THREE.Clock();
@@ -1301,6 +1429,23 @@ function animate() {
}
}
// Animate crystals — gentle float and slow spin
const activity = totalActivity();
for (const crystal of crystals) {
crystal.mesh.position.x = crystal.basePos.x;
crystal.mesh.position.y = crystal.basePos.y + Math.sin(elapsed * 0.65 + crystal.floatPhase) * 0.35;
crystal.mesh.position.z = crystal.basePos.z;
crystal.mesh.rotation.y = elapsed * 0.4 + crystal.floatPhase;
crystal.light.position.copy(crystal.mesh.position);
crystal.light.intensity = 0.2 + activity * 0.8 + Math.sin(elapsed * 2.0 + crystal.floatPhase) * 0.1;
}
// Refresh lightning arcs periodically
if (elapsed * 1000 - lastLightningRefreshTime > LIGHTNING_REFRESH_MS) {
lastLightningRefreshTime = elapsed * 1000;
updateLightningArcs();
}
composer.render();
}