Files
BlackFruit-UI/js/globe.js
yiqiu 69b5c8bebd
All checks were successful
continuous-integration/drone/push Build is passing
达到
2025-11-22 18:39:41 +08:00

300 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 3D 地球效果(首页 banner 右侧)基于 three-globe
// 依赖:全局 THREE 和 ThreeGlobe在 index.html 中通过 CDN 引入)
(function () {
function initGlobe() {
var container = document.getElementById("bannerGlobe");
if (!container) {
console.warn("[Globe] bannerGlobe container not found");
return;
}
if (typeof THREE === "undefined") {
console.warn(
"[Globe] THREE is undefined, please ensure three.min.js is loaded"
);
return;
}
if (typeof ThreeGlobe === "undefined") {
console.warn(
"[Globe] ThreeGlobe is undefined, please ensure three-globe.min.js is loaded"
);
return;
}
var width = container.clientWidth || 400;
var height = container.clientHeight || 400;
// 基础 Three.js 场景
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(40, width / height, 0.1, 1000);
// 初始视角,具体距离在 globe 准备好后根据半径自动调整
camera.position.set(0, 0.2, 8.0);
camera.lookAt(0, 0, 0);
var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(width, height);
renderer.setClearColor(0x000000, 0); // 透明背景
container.appendChild(renderer.domElement);
// 柔和光照
var ambient = new THREE.AmbientLight(0xffffff, 0.65);
scene.add(ambient);
var dirLight = new THREE.DirectionalLight(0xffffff, 0.9);
dirLight.position.set(5, 3, 5);
scene.add(dirLight);
// 节点数据示例(可替换为真实机房经纬度,至少 15 个)
var nodes = [
{ name: "Beijing", lat: 39.9, lng: 116.4 },
{ name: "Shanghai", lat: 31.2, lng: 121.5 },
{ name: "Guangzhou", lat: 23.1, lng: 113.3 },
{ name: "Shenzhen", lat: 22.5, lng: 114.1 },
{ name: "Hong Kong", lat: 22.3, lng: 114.2 },
{ name: "Singapore", lat: 1.3, lng: 103.8 },
{ name: "Tokyo", lat: 35.7, lng: 139.7 },
{ name: "Osaka", lat: 34.7, lng: 135.5 },
{ name: "Seoul", lat: 37.5, lng: 126.9 },
{ name: "Sydney", lat: -33.9, lng: 151.2 },
{ name: "Mumbai", lat: 19.0, lng: 72.8 },
{ name: "Frankfurt", lat: 50.1, lng: 8.7 },
{ name: "London", lat: 51.5, lng: -0.1 },
{ name: "Paris", lat: 48.9, lng: 2.4 },
{ name: "New York", lat: 40.7, lng: -74.0 },
{ name: "San Francisco", lat: 37.8, lng: -122.4 }
];
// 基于节点构造“飞行轨迹”连接数据startLat/startLng -> endLat/endLng
function buildArcsFromNodes(list) {
var arcs = [];
if (!Array.isArray(list) || list.length < 2) {
return arcs;
}
// 简单策略:每个节点连接到后面第 step 个节点,形成环状主干网络
var step = Math.max(1, Math.floor(list.length / 3));
list.forEach(function (src, idx) {
var dst = list[(idx + step) % list.length];
if (!dst || (dst.lat === src.lat && dst.lng === src.lng)) {
return;
}
arcs.push({
startLat: src.lat,
startLng: src.lng,
endLat: dst.lat,
endLng: dst.lng,
// 统一使用偏青色的轨迹,贴合云服务器 / CDN 的科技感
color: "#38BDF8",
});
});
return arcs;
}
// three-globe 实例使用空心陆地多边形hollow globe 风格)
var globe = new ThreeGlobe({
waitForGlobeReady: true,
animateIn: true,
})
// 不使用 three-globe 内置球体贴图,只保留大气层,陆地轮廓由 polygons 层绘制
.showGlobe(false)
.showAtmosphere(false)
.atmosphereColor("#2b9fff")
.atmosphereAltitude(0.18)
.showGraticules(false);
scene.add(globe);
// 构造并应用“节点连接 + 飞行轨迹”效果
var arcs = buildArcsFromNodes(nodes);
if (typeof globe.arcsData === "function" && arcs.length) {
globe.arcsData(arcs);
if (typeof globe.arcColor === "function") {
globe.arcColor(function (d) {
return d.color || "#38BDF8";
});
}
if (typeof globe.arcAltitude === "function") {
globe.arcAltitude(0.2);
}
if (typeof globe.arcStroke === "function") {
globe.arcStroke(0.7);
}
// 使用虚线 + 动画时间制造“飞行”感
if (typeof globe.arcDashLength === "function") {
globe.arcDashLength(0.35);
}
if (typeof globe.arcDashGap === "function") {
globe.arcDashGap(0.8);
}
if (typeof globe.arcDashInitialGap === "function") {
globe.arcDashInitialGap(function () {
return Math.random();
});
}
if (typeof globe.arcDashAnimateTime === "function") {
globe.arcDashAnimateTime(function () {
// 4~7 秒一圈,避免所有轨迹节奏完全一致
return 4000 + Math.random() * 3000;
});
}
}
// 告诉 three-globe 当前渲染器的实际尺寸,避免默认按全窗口大小计算导致比例失真
if (typeof globe.rendererSize === "function") {
globe.rendererSize(new THREE.Vector2(width, height));
}
// 在 globe 初始化完成后,根据实际半径自动调整相机距离,保证整球落在视野内
if (typeof globe.onGlobeReady === "function" && typeof globe.getGlobeRadius === "function") {
globe.onGlobeReady(function () {
var r = globe.getGlobeRadius(); // three-globe 内部使用的球半径
// 经验:以半径的约 3 倍距离拍摄,配合 40° 视角,可以完整显示球体并留一定留白
var dist = r * 3.2;
camera.position.set(0, 0.2, dist);
camera.lookAt(0, 0, 0);
// 在陆地多边形下方放置一个略小的球体作为“海洋”,用白色突出球体轮廓
// 颜色和透明度可以在这里调整
var oceanGeom = new THREE.SphereGeometry(r * 0.99, 64, 64);
var oceanMat = new THREE.MeshPhongMaterial({
color: "#FFFFFF", // 海洋/球体底色:白色(可改成其他颜色)
transparent: true,
opacity: 1, // 透明度:越接近 1 越实
shininess: 40,
side: THREE.FrontSide,
});
var oceanMesh = new THREE.Mesh(oceanGeom, oceanMat);
globe.add(oceanMesh);
// 在节点位置放置小型静态“节点点”
var nodeGeom = new THREE.SphereGeometry(r * 0.02, 16, 16);
var nodeMat = new THREE.MeshBasicMaterial({
color: "#38BDF8",
transparent: true,
opacity: 0.9,
});
// 通知 three-globe 当前视角,用于部分图层的内部计算
if (typeof globe.setPointOfView === "function") {
globe.setPointOfView(camera);
}
// 使用 globe 的工具方法,将经纬度转换为球面上的 3D 坐标
var hasGetCoords = typeof globe.getCoords === "function";
nodes.forEach(function (n) {
var pos;
if (hasGetCoords) {
// altitude 略高于球面,避免被多边形遮挡
pos = globe.getCoords(n.lat, n.lng, 0.02);
} else {
// 兼容降级:简单根据半径和经纬度计算
var phi = (90 - n.lat) * (Math.PI / 180);
var theta = (n.lng + 180) * (Math.PI / 180);
var rr = r * 1.02;
pos = {
x: -rr * Math.sin(phi) * Math.cos(theta),
z: rr * Math.sin(phi) * Math.sin(theta),
y: rr * Math.cos(phi)
};
}
var nodeMesh = new THREE.Mesh(nodeGeom, nodeMat);
nodeMesh.position.set(pos.x, pos.y, pos.z);
globe.add(nodeMesh);
});
});
}
// 按 hollow-globe 风格加载陆地多边形,绘制空心地球轮廓
(function loadLandPolygons() {
if (typeof topojson === "undefined") {
console.warn(
"[Globe] topojson-client is undefined, unable to render hollow globe polygons"
);
return;
}
// 请确保 /web/BlackFruit-web/assets/data/land-110m.json 存在,
// 内容为你刚才提供的 Topology JSON。
fetch("/web/BlackFruit-web/assets/data/land-110m.json")
.then(function (res) {
return res.json();
})
.then(function (landTopo) {
try {
var landGeo = topojson.feature(
landTopo,
landTopo.objects.land
).features;
globe
.polygonsData(landGeo)
.polygonCapMaterial(
new THREE.MeshLambertMaterial({
// 更亮、更偏青的科技蓝陆地,与轨迹/节点颜色统一
color: "#38BDF8",
transparent: true,
opacity: 0.75,
side: THREE.DoubleSide,
})
)
.polygonSideColor(function () {
return "rgba(0,0,0,0)";
});
} catch (e) {
console.warn("[Globe] failed to parse land-110m topology:", e);
}
})
.catch(function (err) {
console.warn(
"[Globe] failed to load /web/BlackFruit-web/assets/data/land-110m.json:",
err
);
});
})();
// 暴露给调试用
if (typeof window !== "undefined") {
window.__globe = globe;
}
// 自适应窗口大小
function onResize() {
if (!container) return;
var w = container.clientWidth || width;
var h = container.clientHeight || height;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
// 同步 three-globe 对 renderer 尺寸的感知,保证缩放比例一致
if (typeof globe.rendererSize === "function") {
globe.rendererSize(new THREE.Vector2(w, h));
}
}
window.addEventListener("resize", onResize);
// 动画循环
var lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
var now = performance.now();
var delta = (now - lastTime) / 1000;
lastTime = now;
// 地球自转
globe.rotation.y += delta * 0.25;
renderer.render(scene, camera);
}
animate();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initGlobe);
} else {
initGlobe();
}
})();