[claude] Lightning arcs between floating crystals during high activity (#243) #348
66
app.js
66
app.js
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user