[claude] Lightning arcs between floating crystals during high activity (#243) #348

Merged
claude merged 1 commits from claude/issue-243 into main 2026-03-24 05:11:43 +00:00

66
app.js
View File

@@ -1114,7 +1114,7 @@ for (let i = 0; i < CRYSTAL_COUNT; i++) {
light.position.copy(basePos);
crystalGroup.add(light);
crystals.push({ mesh, light, basePos, floatPhase: (i / CRYSTAL_COUNT) * Math.PI * 2 });
crystals.push({ mesh, light, basePos, floatPhase: (i / CRYSTAL_COUNT) * Math.PI * 2, flashStartTime: -999 });
}
// ---- Lightning arc pool ----
@@ -1127,6 +1127,12 @@ let lastLightningRefreshTime = 0;
/** @type {Array<THREE.Line>} */
const lightningArcs = [];
/**
* Per-arc runtime state for per-frame flicker.
* @type {Array<{active: boolean, baseOpacity: number, srcIdx: number, dstIdx: number}>}
*/
const lightningArcMeta = [];
for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const positions = new Float32Array((LIGHTNING_SEGMENTS + 1) * 3);
const geo = new THREE.BufferGeometry();
@@ -1141,6 +1147,7 @@ for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const arc = new THREE.Line(geo, mat);
scene.add(arc);
lightningArcs.push(arc);
lightningArcMeta.push({ active: false, baseOpacity: 0, srcIdx: 0, dstIdx: 0 });
}
/**
@@ -1171,16 +1178,35 @@ function totalActivity() {
}
/**
* Refreshes lightning arcs based on current activity level.
* Lerps between two hex colors. t=0 → colorA, t=1 → colorB.
* @param {number} colorA
* @param {number} colorB
* @param {number} t
* @returns {number}
*/
function updateLightningArcs() {
function lerpColor(colorA, colorB, t) {
const ar = (colorA >> 16) & 0xff, ag = (colorA >> 8) & 0xff, ab = colorA & 0xff;
const br = (colorB >> 16) & 0xff, bg = (colorB >> 8) & 0xff, bb = colorB & 0xff;
const r = Math.round(ar + (br - ar) * t);
const g = Math.round(ag + (bg - ag) * t);
const b = Math.round(ab + (bb - ab) * t);
return (r << 16) | (g << 8) | b;
}
/**
* Refreshes lightning arc geometry and metadata based on current activity level.
* @param {number} elapsed Current clock time in seconds.
*/
function updateLightningArcs(elapsed) {
const activity = totalActivity();
const activeCount = Math.round(activity * LIGHTNING_POOL_SIZE);
for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const arc = lightningArcs[i];
const meta = lightningArcMeta[i];
if (i >= activeCount) {
arc.material.opacity = 0;
meta.active = false;
continue;
}
@@ -1194,8 +1220,19 @@ function updateLightningArcs() {
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)]);
// Blend arc color between source and destination crystal
arc.material.color.setHex(lerpColor(CRYSTAL_COLORS[a], CRYSTAL_COLORS[b], 0.5));
const base = (0.35 + Math.random() * 0.55) * Math.min(activity * 1.5, 1.0);
arc.material.opacity = base;
meta.active = true;
meta.baseOpacity = base;
meta.srcIdx = a;
meta.dstIdx = b;
// Trigger brief emissive flash on both struck crystals
crystals[a].flashStartTime = elapsed;
crystals[b].flashStartTime = elapsed;
}
}
@@ -1427,7 +1464,7 @@ function animate() {
}
}
// Animate crystals — gentle float and slow spin
// Animate crystals — gentle float, slow spin, and lightning-strike flash
const activity = totalActivity();
for (const crystal of crystals) {
crystal.mesh.position.x = crystal.basePos.x;
@@ -1435,13 +1472,24 @@ function animate() {
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;
const flashAge = elapsed - crystal.flashStartTime;
const flashBoost = flashAge < 0.25 ? (1.0 - flashAge / 0.25) * 2.0 : 0.0;
crystal.light.intensity = 0.2 + activity * 0.8 + Math.sin(elapsed * 2.0 + crystal.floatPhase) * 0.1 + flashBoost;
crystal.mesh.material.emissiveIntensity = 1.0 + flashBoost * 0.8;
}
// Refresh lightning arcs periodically
// Per-frame lightning flicker — modulate opacity each frame for realism
for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const meta = lightningArcMeta[i];
if (meta.active) {
lightningArcs[i].material.opacity = meta.baseOpacity * (0.55 + Math.random() * 0.45);
}
}
// Refresh lightning arc geometry periodically
if (elapsed * 1000 - lastLightningRefreshTime > LIGHTNING_REFRESH_MS) {
lastLightningRefreshTime = elapsed * 1000;
updateLightningArcs();
updateLightningArcs(elapsed);
}
composer.render();