LOADING EARTH ONLINE
${c.n}${c.pop.toLocaleString()}`;
div.onclick = ()=>focusCity(i);
cityListEl.appendChild(div);
});
function focusCity(i){
const city = CITIES[i];
$('#ic-name').textContent = city.n;
$('#ic-country').textContent = city.c + ' · ' + city.e;
$('#ic-lat').textContent = city.lat.toFixed(4) + '°';
$('#ic-lng').textContent = city.lng.toFixed(4) + '°';
$('#ic-pop').textContent = city.pop.toLocaleString();
$('#ic-tz').textContent = city.tz;
$('#ic-time').textContent = getCityTime(city);
$('#ic-users').textContent = (Math.floor(city.pop*0.0003+Math.random()*5000)).toLocaleString();
// 设置信息卡中的大国旗
const flagEl = $('#ic-flag');
flagEl.style.backgroundImage = `url(flags/${city.cc}.png)`;
flagEl.classList.remove('wave'); void flagEl.offsetWidth; flagEl.classList.add('wave');
// 标记3D高亮旗帜
activeFlag = flagMeshes.find(f => f.userData.city && f.userData.city.n === city.n) || null;
if(activeFlag){
activeFlag.userData.waveBoost = 1;
activeFlag.userData.fadeIn = 0;
}
$('#infoCard').classList.add('show');
// 视角飞向
const p = latLngToVec3(city.lat, city.lng, R);
const camPos = p.clone().multiplyScalar(3.0);
animateCamera(camPos, p.clone().multiplyScalar(0));
}
window.hideInfo = ()=>$('#infoCard').classList.remove('show');
function getCityTime(city){
const offset = parseFloat(city.tz.replace('UTC','').replace(':','.')||'0');
const now = new Date();
const utc = now.getTime() + now.getTimezoneOffset()*60000;
const t = new Date(utc + offset*3600000);
return `${fmt(t.getHours())}:${fmt(t.getMinutes())}:${fmt(t.getSeconds())}`;
}
let camTarget = null, camPosTarget = null, camAnim = null;
function animateCamera(toPos, toTarget){
camPosTarget = toPos; camTarget = toTarget;
camAnim = {t:0, from:camera.position.clone(), fromT:controls.target.clone()};
}
function updateCameraAnim(dt){
if(!camAnim) return;
camAnim.t += dt*0.6;
if(camAnim.t >= 1){
camAnim = null; camTarget = null; camPosTarget = null;
return;
}
const t = camAnim.t;
const e = t<0.5 ? 2*t*t : 1-Math.pow(-2*t+2,2)/2;
camera.position.lerpVectors(camAnim.from, camPosTarget, e);
controls.target.lerpVectors(camAnim.fromT, camTarget, e);
}
/* 鼠标点击 */
const ray = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let activeFlag = null; // 当前高亮飘动的旗帜
renderer.domElement.addEventListener('click', e=>{
if(e.target !== renderer.domElement) return;
mouse.x = (e.clientX/innerWidth)*2-1;
mouse.y = -(e.clientY/innerHeight)*2+1;
ray.setFromCamera(mouse, camera);
// 同时检测城市圆点和旗帜
const targets = cityMeshes.filter(m=>
(m.geometry.type==='SphereGeometry' && m.geometry.parameters.radius<0.02) ||
(m.userData && m.userData.isFlag)
);
const hits = ray.intersectObjects(targets);
if(hits.length){
const obj = hits[0].object;
const city = obj.userData.city || obj.userData;
focusCity(CITIES.findIndex(c=>c.n===city.n));
}
});
/* ===========================================================
控件
=========================================================== */
let autoRotate=true, showClouds=true, showStars=true, showAtm=true, showLines=true, showFlags=true, timeSpeed=1;
$('#btnRotate').onclick = e=>{ autoRotate=!autoRotate; e.target.classList.toggle('active',autoRotate); };
$('#btnClouds').onclick = e=>{ showClouds=!showClouds; clouds.visible=showClouds; e.target.classList.toggle('active',showClouds); };
$('#btnStars').onclick = e=>{ showStars=!showStars; stars.visible=showStars; e.target.classList.toggle('active',showStars); };
$('#btnAtm').onclick = e=>{ showAtm=!showAtm; atmo.visible=showAtm; halo.visible=showAtm; e.target.classList.toggle('active',showAtm); };
$('#btnLines').onclick = e=>{ showLines=!showLines; lineGroup.visible=showLines; particles.forEach(p=>p.visible=showLines); e.target.classList.toggle('active',showLines); };
$('#btnFlags').onclick = e=>{
showFlags=!showFlags;
flagMeshes.forEach(f=>f.visible=showFlags);
e.target.classList.toggle('active',showFlags);
};
const speeds=[1,60,600,3600];
let spIdx=0;
$('#btnSpeed').onclick = e=>{
spIdx=(spIdx+1)%speeds.length;
timeSpeed=speeds[spIdx];
e.target.textContent = `时间 ×${timeSpeed}`;
e.target.classList.toggle('active', timeSpeed!==1);
};
$('#btnReset').onclick = ()=>{
animateCamera(new THREE.Vector3(0,1.5,4.5), new THREE.Vector3(0,0,0));
};
// 默认激活态
['btnRotate','btnClouds','btnStars','btnAtm','btnLines','btnFlags'].forEach(id=>$('#'+id).classList.add('active'));
/* ===========================================================
动画
=========================================================== */
let simTime = Date.now();
const clock = new THREE.Clock();
let frame=0, lastFps=performance.now(), fps=0;
function tick(){
const dt = clock.getDelta();
simTime += dt*1000*timeSpeed;
// 自转
if(autoRotate && !camAnim){
earth.rotation.y += dt*0.05;
clouds.rotation.y += dt*0.06;
}
if(!camAnim){
// 鼠标控制
controls.update();
} else {
updateCameraAnim(dt);
}
// 太阳位置(时间驱动)
const ang = (simTime / (86400000)) * Math.PI*2;
sun.position.set(Math.cos(ang)*8, Math.sin(ang*0.5)*2, Math.sin(ang)*8);
sun.intensity = Math.max(0, Math.sin(ang))*1.4 + 0.3;
// 城市脉冲环
cityMeshes.forEach(m=>{
if(m.geometry.type==='RingGeometry'){
const s = 1 + Math.sin(simTime*0.003 + m.userData.pulse)*0.5;
m.scale.setScalar(s);
m.material.opacity = 0.3 + (1.5-s)*0.4;
}
});
// 国旗飘动 + 始终面向相机
flagMeshes.forEach(flag=>{
flag.material.uniforms.uTime.value = simTime*0.001;
// waveBoost 衰减
if(flag.userData.waveBoost > 0){
flag.userData.waveBoost = Math.max(0, flag.userData.waveBoost - dt*0.5);
}
flag.material.uniforms.uWave.value = 1.0 + flag.userData.waveBoost*2.0;
// 始终面向相机(billboard)
flag.lookAt(camera.position);
});
// 粒子沿连线流动
particles.forEach(p=>{
if(!p.visible) return;
p.userData.t += dt*0.4*timeSpeed;
if(p.userData.t > 1) p.userData.t -= 1;
const pos = p.userData.line.geometry.attributes.position;
const idx = Math.floor(p.userData.t*(pos.count-1));
p.position.fromBufferAttribute(pos, idx);
});
// 星空缓慢旋转
if(stars) stars.rotation.y += dt*0.0008;
renderer.render(scene, camera);
// FPS
frame++;
if(performance.now()-lastFps>=500){
fps=Math.round(frame*1000/(performance.now()-lastFps));
$('#fps').textContent=fps;
lastFps=performance.now();
frame=0;
}
// UTC
const d = new Date(simTime);
$('#utc').textContent = `${fmt(d.getUTCHours())}:${fmt(d.getUTCMinutes())}:${fmt(d.getUTCSeconds())}`;
$('#online').textContent = (1250000 + Math.floor(Math.sin(simTime*0.0001)*50000)).toLocaleString();
requestAnimationFrame(tick);
}
/* ===========================================================
自适应
=========================================================== */
addEventListener('resize', ()=>{
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
/* 启动 */
setTimeout(()=>{
$('#loader').style.opacity='0';
setTimeout(()=>$('#loader').remove(), 500);
tick();
}, 300);