feat: add meteor shower effect triggering randomly every 2-5 min
Implements a meteor shower system in the Three.js scene: - Each shower spawns 8-17 meteors with staggered delays (0.18–0.46s apart) - Meteors streak as bright cyan-white lines (tail + head) across the sky - Per-shower direction keeps streaks coherent; slight per-meteor variation - Meteors fade out over the last 35% of their ~1.3-1.9s lifetime - First shower fires 15-30s after page load; subsequent showers every 2-5 min - Geometry/material disposed on completion to avoid memory leaks Fixes #113 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
125
app.js
125
app.js
@@ -575,6 +575,92 @@ async function loadSovereigntyStatus() {
|
||||
|
||||
loadSovereigntyStatus();
|
||||
|
||||
// === METEOR SHOWER ===
|
||||
const METEOR_COLOR = 0xaaddff;
|
||||
const METEOR_SPEED_BASE = 55; // units/sec
|
||||
const METEOR_TAIL_BASE = 7; // world units
|
||||
const METEOR_DURATION_BASE = 1.3; // seconds
|
||||
|
||||
/** @type {Array<{line: THREE.Line, startPos: THREE.Vector3, dir: THREE.Vector3, spawnTime: number, duration: number, speed: number, tailLength: number}>} */
|
||||
const activeMeteors = [];
|
||||
|
||||
/** @type {Array<{triggerTime: number, delay: number, showerDir: THREE.Vector3}>} */
|
||||
const scheduledMeteors = [];
|
||||
|
||||
// First shower triggers 15–30 s after load so it's visible on first visit;
|
||||
// subsequent showers every 2–5 minutes.
|
||||
let nextMeteorShowerAt = 15 + Math.random() * 15;
|
||||
|
||||
/**
|
||||
* Spawns a single meteor line in world space.
|
||||
* @param {number} elapsed
|
||||
* @param {THREE.Vector3} showerDir
|
||||
*/
|
||||
function spawnMeteor(elapsed, showerDir) {
|
||||
const r = 140 + Math.random() * 60;
|
||||
const phi = Math.random() * Math.PI * 2;
|
||||
const elevAngle = (Math.random() * 0.4 + 0.1); // 0.1–0.5 rad above equator
|
||||
const startPos = new THREE.Vector3(
|
||||
r * Math.cos(elevAngle) * Math.cos(phi),
|
||||
r * Math.sin(elevAngle) + 20,
|
||||
r * Math.cos(elevAngle) * Math.sin(phi)
|
||||
);
|
||||
|
||||
const dir = showerDir.clone().add(
|
||||
new THREE.Vector3(
|
||||
(Math.random() - 0.5) * 0.25,
|
||||
(Math.random() - 0.5) * 0.25,
|
||||
(Math.random() - 0.5) * 0.25
|
||||
)
|
||||
).normalize();
|
||||
|
||||
const positions = new Float32Array(6);
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
|
||||
const mat = new THREE.LineBasicMaterial({
|
||||
color: METEOR_COLOR,
|
||||
transparent: true,
|
||||
opacity: 1.0,
|
||||
depthWrite: false,
|
||||
});
|
||||
|
||||
const line = new THREE.Line(geo, mat);
|
||||
scene.add(line);
|
||||
|
||||
activeMeteors.push({
|
||||
line,
|
||||
startPos,
|
||||
dir,
|
||||
spawnTime: elapsed,
|
||||
duration: METEOR_DURATION_BASE + Math.random() * 0.6,
|
||||
speed: METEOR_SPEED_BASE + Math.random() * 25,
|
||||
tailLength: METEOR_TAIL_BASE + Math.random() * 5,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a burst of meteors with staggered spawn delays.
|
||||
* @param {number} elapsed
|
||||
*/
|
||||
function triggerMeteorShower(elapsed) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const showerDir = new THREE.Vector3(
|
||||
Math.cos(angle) * 0.65,
|
||||
-0.55 - Math.random() * 0.25,
|
||||
Math.sin(angle) * 0.65
|
||||
).normalize();
|
||||
|
||||
const count = 8 + Math.floor(Math.random() * 10);
|
||||
for (let i = 0; i < count; i++) {
|
||||
scheduledMeteors.push({
|
||||
triggerTime: elapsed,
|
||||
delay: i * (0.18 + Math.random() * 0.28),
|
||||
showerDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// === ANIMATION LOOP ===
|
||||
const clock = new THREE.Clock();
|
||||
|
||||
@@ -656,6 +742,45 @@ function animate() {
|
||||
sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22;
|
||||
}
|
||||
|
||||
// Trigger meteor shower on schedule
|
||||
if (elapsed >= nextMeteorShowerAt) {
|
||||
triggerMeteorShower(elapsed);
|
||||
nextMeteorShowerAt = elapsed + 120 + Math.random() * 180; // 2–5 min
|
||||
}
|
||||
|
||||
// Spawn any scheduled meteors whose delay has elapsed
|
||||
for (let i = scheduledMeteors.length - 1; i >= 0; i--) {
|
||||
const sm = scheduledMeteors[i];
|
||||
if (elapsed >= sm.triggerTime + sm.delay) {
|
||||
spawnMeteor(elapsed, sm.showerDir);
|
||||
scheduledMeteors.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Animate active meteors
|
||||
for (let i = activeMeteors.length - 1; i >= 0; i--) {
|
||||
const m = activeMeteors[i];
|
||||
const age = elapsed - m.spawnTime;
|
||||
if (age >= m.duration) {
|
||||
scene.remove(m.line);
|
||||
m.line.geometry.dispose();
|
||||
m.line.material.dispose();
|
||||
activeMeteors.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
const headDist = age * m.speed;
|
||||
const tailDist = Math.max(0, headDist - m.tailLength);
|
||||
const head = m.startPos.clone().addScaledVector(m.dir, headDist);
|
||||
const tail = m.startPos.clone().addScaledVector(m.dir, tailDist);
|
||||
const pos = m.line.geometry.attributes.position.array;
|
||||
pos[0] = tail.x; pos[1] = tail.y; pos[2] = tail.z;
|
||||
pos[3] = head.x; pos[4] = head.y; pos[5] = head.z;
|
||||
m.line.geometry.attributes.position.needsUpdate = true;
|
||||
// Fade out in last 35% of lifetime
|
||||
const t = age / m.duration;
|
||||
m.line.material.opacity = t > 0.65 ? (1 - t) / 0.35 : 1.0;
|
||||
}
|
||||
|
||||
composer.render();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user