This commit is contained in:
333
js/globe.js
333
js/globe.js
@@ -1,5 +1,6 @@
|
|||||||
// 3D 地球效果(首页 banner 右侧)基于 three-globe
|
// 首页 3D 粒子地球(banner 右侧)
|
||||||
// 依赖:全局 THREE 和 ThreeGlobe(在 index.html 中通过 CDN 引入)
|
// 改造为:大量粒子从远处飞入,最终聚合成一个球体,并缓慢自转
|
||||||
|
// 依赖:全局 THREE(在 index.html 中通过 CDN 引入)
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
function initGlobe() {
|
function initGlobe() {
|
||||||
@@ -14,12 +15,6 @@
|
|||||||
);
|
);
|
||||||
return;
|
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 width = container.clientWidth || 400;
|
||||||
var height = container.clientHeight || 400;
|
var height = container.clientHeight || 400;
|
||||||
@@ -28,8 +23,8 @@
|
|||||||
var scene = new THREE.Scene();
|
var scene = new THREE.Scene();
|
||||||
|
|
||||||
var camera = new THREE.PerspectiveCamera(40, width / height, 0.1, 1000);
|
var camera = new THREE.PerspectiveCamera(40, width / height, 0.1, 1000);
|
||||||
// 初始视角,具体距离在 globe 准备好后根据半径自动调整
|
// 初始视角,稍微偏上俯视球体
|
||||||
camera.position.set(0, 0.2, 8.0);
|
camera.position.set(0, 0.8, 7.5);
|
||||||
camera.lookAt(0, 0, 0);
|
camera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||||
@@ -45,217 +40,112 @@
|
|||||||
dirLight.position.set(5, 3, 5);
|
dirLight.position.set(5, 3, 5);
|
||||||
scene.add(dirLight);
|
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 PARTICLE_COUNT = 2200; // 粒子数量,越多越实,但性能负担也越大
|
||||||
var arcs = [];
|
var SPHERE_RADIUS = 2.4; // 球体半径(世界单位)
|
||||||
if (!Array.isArray(list) || list.length < 2) {
|
var START_RADIUS_MIN = 5.0; // 粒子起始距离下限
|
||||||
return arcs;
|
var START_RADIUS_MAX = 9.0; // 粒子起始距离上限
|
||||||
}
|
var FORMATION_DURATION = 4000; // 粒子从散开到聚合成球的时间(毫秒)
|
||||||
// 简单策略:每个节点连接到后面第 step 个节点,形成环状主干网络
|
var ROTATION_SPEED = 0.25; // 成形后的自转速度(弧度/秒,约等于旧地球的速度)
|
||||||
var step = Math.max(1, Math.floor(list.length / 3));
|
|
||||||
list.forEach(function (src, idx) {
|
// 准备缓动函数(飞入时用 easeOutCubic,让粒子接近球面时减速)
|
||||||
var dst = list[(idx + step) % list.length];
|
function easeOutCubic(t) {
|
||||||
if (!dst || (dst.lat === src.lat && dst.lng === src.lng)) {
|
return 1 - Math.pow(1 - t, 3);
|
||||||
return;
|
|
||||||
}
|
|
||||||
arcs.push({
|
|
||||||
startLat: src.lat,
|
|
||||||
startLng: src.lng,
|
|
||||||
endLat: dst.lat,
|
|
||||||
endLng: dst.lng,
|
|
||||||
// 统一使用偏青色的轨迹,贴合云服务器 / CDN 的科技感
|
|
||||||
color: "#38BDF8",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return arcs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// three-globe 实例:使用空心陆地多边形(hollow globe 风格)
|
// 准备粒子数据:startPositions / targetPositions / current positions
|
||||||
var globe = new ThreeGlobe({
|
var geometry = new THREE.BufferGeometry();
|
||||||
waitForGlobeReady: true,
|
var positions = new Float32Array(PARTICLE_COUNT * 3);
|
||||||
animateIn: true,
|
var startPositions = new Float32Array(PARTICLE_COUNT * 3);
|
||||||
})
|
var targetPositions = new Float32Array(PARTICLE_COUNT * 3);
|
||||||
// 不使用 three-globe 内置球体贴图,只保留大气层,陆地轮廓由 polygons 层绘制
|
|
||||||
.showGlobe(false)
|
|
||||||
.showAtmosphere(false)
|
|
||||||
.atmosphereColor("#2b9fff")
|
|
||||||
.atmosphereAltitude(0.18)
|
|
||||||
.showGraticules(false);
|
|
||||||
|
|
||||||
scene.add(globe);
|
// 随机在球壳表面生成点(均匀分布),作为目标位置
|
||||||
|
function randomPointOnSphere(radius) {
|
||||||
// 构造并应用“节点连接 + 飞行轨迹”效果
|
// 使用均匀球面采样:theta = 2πu, phi = arccos(2v - 1)
|
||||||
var arcs = buildArcsFromNodes(nodes);
|
var u = Math.random();
|
||||||
if (typeof globe.arcsData === "function" && arcs.length) {
|
var v = Math.random();
|
||||||
globe.arcsData(arcs);
|
var theta = 2 * Math.PI * u;
|
||||||
if (typeof globe.arcColor === "function") {
|
var phi = Math.acos(2 * v - 1);
|
||||||
globe.arcColor(function (d) {
|
var sinPhi = Math.sin(phi);
|
||||||
return d.color || "#38BDF8";
|
var x = radius * sinPhi * Math.cos(theta);
|
||||||
});
|
var y = radius * Math.cos(phi);
|
||||||
}
|
var z = radius * sinPhi * Math.sin(theta);
|
||||||
if (typeof globe.arcAltitude === "function") {
|
return { x: x, y: y, z: z };
|
||||||
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 当前渲染器的实际尺寸,避免默认按全窗口大小计算导致比例失真
|
function randomPointFarFromCenter(minR, maxR) {
|
||||||
if (typeof globe.rendererSize === "function") {
|
var radius = minR + Math.random() * (maxR - minR);
|
||||||
globe.rendererSize(new THREE.Vector2(width, height));
|
var u = Math.random();
|
||||||
|
var v = Math.random();
|
||||||
|
var theta = 2 * Math.PI * u;
|
||||||
|
var phi = Math.acos(2 * v - 1);
|
||||||
|
var sinPhi = Math.sin(phi);
|
||||||
|
var x = radius * sinPhi * Math.cos(theta);
|
||||||
|
var y = radius * Math.cos(phi);
|
||||||
|
var z = radius * sinPhi * Math.sin(theta);
|
||||||
|
return { x: x, y: y, z: z };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在 globe 初始化完成后,根据实际半径自动调整相机距离,保证整球落在视野内
|
for (var i = 0; i < PARTICLE_COUNT; i++) {
|
||||||
if (typeof globe.onGlobeReady === "function" && typeof globe.getGlobeRadius === "function") {
|
var i3 = i * 3;
|
||||||
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 target = randomPointOnSphere(SPHERE_RADIUS);
|
||||||
// 颜色和透明度可以在这里调整
|
targetPositions[i3] = target.x;
|
||||||
var oceanGeom = new THREE.SphereGeometry(r * 0.99, 64, 64);
|
targetPositions[i3 + 1] = target.y;
|
||||||
var oceanMat = new THREE.MeshPhongMaterial({
|
targetPositions[i3 + 2] = target.z;
|
||||||
color: "#FFFFFF", // 海洋/球体底色:白色(可改成其他颜色)
|
|
||||||
transparent: true,
|
|
||||||
opacity: 1, // 透明度:越接近 1 越实
|
|
||||||
shininess: 40,
|
|
||||||
side: THREE.FrontSide,
|
|
||||||
});
|
|
||||||
var oceanMesh = new THREE.Mesh(oceanGeom, oceanMat);
|
|
||||||
globe.add(oceanMesh);
|
|
||||||
|
|
||||||
// 在节点位置放置小型静态“节点点”
|
var start = randomPointFarFromCenter(START_RADIUS_MIN, START_RADIUS_MAX);
|
||||||
var nodeGeom = new THREE.SphereGeometry(r * 0.02, 16, 16);
|
startPositions[i3] = start.x;
|
||||||
var nodeMat = new THREE.MeshBasicMaterial({
|
startPositions[i3 + 1] = start.y;
|
||||||
color: "#38BDF8",
|
startPositions[i3 + 2] = start.z;
|
||||||
transparent: true,
|
|
||||||
opacity: 0.9,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 通知 three-globe 当前视角,用于部分图层的内部计算
|
// 初始位置就是起始位置(粒子还未飞入)
|
||||||
if (typeof globe.setPointOfView === "function") {
|
positions[i3] = start.x;
|
||||||
globe.setPointOfView(camera);
|
positions[i3 + 1] = start.y;
|
||||||
}
|
positions[i3 + 2] = start.z;
|
||||||
|
|
||||||
// 使用 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 风格加载陆地多边形,绘制空心地球轮廓
|
geometry.setAttribute(
|
||||||
(function loadLandPolygons() {
|
"position",
|
||||||
if (typeof topojson === "undefined") {
|
new THREE.BufferAttribute(positions, 3)
|
||||||
console.warn(
|
);
|
||||||
"[Globe] topojson-client is undefined, unable to render hollow globe polygons"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 请确保 /web/BlackFruit-web/assets/data/land-110m.json 存在,
|
var material = new THREE.PointsMaterial({
|
||||||
// 内容为你刚才提供的 Topology JSON。
|
color: 0x38bdf8, // 粒子主色(可自行调整)
|
||||||
fetch("/web/BlackFruit-web/assets/data/land-110m.json")
|
size: 0.06, // 每个粒子在世界中的尺寸
|
||||||
.then(function (res) {
|
sizeAttenuation: true,
|
||||||
return res.json();
|
transparent: true,
|
||||||
})
|
opacity: 0.95,
|
||||||
.then(function (landTopo) {
|
depthWrite: false,
|
||||||
try {
|
blending: THREE.AdditiveBlending,
|
||||||
var landGeo = topojson.feature(
|
});
|
||||||
landTopo,
|
|
||||||
landTopo.objects.land
|
|
||||||
).features;
|
|
||||||
|
|
||||||
globe
|
var particleSphere = new THREE.Points(geometry, material);
|
||||||
.polygonsData(landGeo)
|
scene.add(particleSphere);
|
||||||
.polygonCapMaterial(
|
|
||||||
new THREE.MeshLambertMaterial({
|
// 也可以增加一个非常淡的内层球体,进一步强调轮廓(如不需要可注释掉)
|
||||||
// 更亮、更偏青的科技蓝陆地,与轨迹/节点颜色统一
|
var innerSphereGeom = new THREE.SphereGeometry(SPHERE_RADIUS * 0.98, 48, 48);
|
||||||
color: "#38BDF8",
|
var innerSphereMat = new THREE.MeshBasicMaterial({
|
||||||
transparent: true,
|
color: 0x0f172a,
|
||||||
opacity: 0.75,
|
transparent: true,
|
||||||
side: THREE.DoubleSide,
|
opacity: 0.25,
|
||||||
})
|
side: THREE.BackSide,
|
||||||
)
|
});
|
||||||
.polygonSideColor(function () {
|
var innerSphereMesh = new THREE.Mesh(innerSphereGeom, innerSphereMat);
|
||||||
return "rgba(0,0,0,0)";
|
scene.add(innerSphereMesh);
|
||||||
});
|
|
||||||
} 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") {
|
if (typeof window !== "undefined") {
|
||||||
window.__globe = globe;
|
window.__particleGlobe = {
|
||||||
|
scene: scene,
|
||||||
|
camera: camera,
|
||||||
|
renderer: renderer,
|
||||||
|
particleSphere: particleSphere,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自适应窗口大小
|
// 自适应窗口大小
|
||||||
@@ -266,24 +156,45 @@
|
|||||||
camera.aspect = w / h;
|
camera.aspect = w / h;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
renderer.setSize(w, h);
|
renderer.setSize(w, h);
|
||||||
// 同步 three-globe 对 renderer 尺寸的感知,保证缩放比例一致
|
|
||||||
if (typeof globe.rendererSize === "function") {
|
|
||||||
globe.rendererSize(new THREE.Vector2(w, h));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
window.addEventListener("resize", onResize);
|
window.addEventListener("resize", onResize);
|
||||||
|
|
||||||
// 动画循环
|
// 动画循环:粒子从远处飞入,逐渐聚合成球体,并缓慢自转
|
||||||
var lastTime = performance.now();
|
var startTime = performance.now();
|
||||||
|
var lastTime = startTime;
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
|
|
||||||
var now = performance.now();
|
var now = performance.now();
|
||||||
var delta = (now - lastTime) / 1000;
|
var delta = (now - lastTime) / 1000;
|
||||||
|
var elapsed = now - startTime;
|
||||||
lastTime = now;
|
lastTime = now;
|
||||||
|
|
||||||
// 地球自转
|
// 粒子从起始位置插值到球面目标位置
|
||||||
globe.rotation.y += delta * 0.25;
|
var t = Math.min(1, elapsed / FORMATION_DURATION);
|
||||||
|
var eased = easeOutCubic(t);
|
||||||
|
|
||||||
|
var i, i3;
|
||||||
|
for (i = 0; i < PARTICLE_COUNT; i++) {
|
||||||
|
i3 = i * 3;
|
||||||
|
var sx = startPositions[i3];
|
||||||
|
var sy = startPositions[i3 + 1];
|
||||||
|
var sz = startPositions[i3 + 2];
|
||||||
|
|
||||||
|
var tx = targetPositions[i3];
|
||||||
|
var ty = targetPositions[i3 + 1];
|
||||||
|
var tz = targetPositions[i3 + 2];
|
||||||
|
|
||||||
|
positions[i3] = sx + (tx - sx) * eased;
|
||||||
|
positions[i3 + 1] = sy + (ty - sy) * eased;
|
||||||
|
positions[i3 + 2] = sz + (tz - sz) * eased;
|
||||||
|
}
|
||||||
|
geometry.attributes.position.needsUpdate = true;
|
||||||
|
|
||||||
|
// 整体自转
|
||||||
|
particleSphere.rotation.y += ROTATION_SPEED * delta;
|
||||||
|
innerSphereMesh.rotation.y += ROTATION_SPEED * delta * 0.9;
|
||||||
|
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user