From 9011cec580728635a575836ce4a0b4940ab746ad Mon Sep 17 00:00:00 2001 From: yiqiu Date: Sat, 22 Nov 2025 18:44:13 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/globe.js | 333 +++++++++++++++++++--------------------------------- 1 file changed, 122 insertions(+), 211 deletions(-) diff --git a/js/globe.js b/js/globe.js index 336ce5f..ebce617 100644 --- a/js/globe.js +++ b/js/globe.js @@ -1,5 +1,6 @@ -// 3D 地球效果(首页 banner 右侧)基于 three-globe -// 依赖:全局 THREE 和 ThreeGlobe(在 index.html 中通过 CDN 引入) +// 首页 3D 粒子地球(banner 右侧) +// 改造为:大量粒子从远处飞入,最终聚合成一个球体,并缓慢自转 +// 依赖:全局 THREE(在 index.html 中通过 CDN 引入) (function () { function initGlobe() { @@ -14,12 +15,6 @@ ); 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; @@ -28,8 +23,8 @@ 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.position.set(0, 0.8, 7.5); camera.lookAt(0, 0, 0); var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); @@ -45,217 +40,112 @@ 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; + // 可调参数:你可以根据效果需求自行修改 + var PARTICLE_COUNT = 2200; // 粒子数量,越多越实,但性能负担也越大 + var SPHERE_RADIUS = 2.4; // 球体半径(世界单位) + var START_RADIUS_MIN = 5.0; // 粒子起始距离下限 + var START_RADIUS_MAX = 9.0; // 粒子起始距离上限 + var FORMATION_DURATION = 4000; // 粒子从散开到聚合成球的时间(毫秒) + var ROTATION_SPEED = 0.25; // 成形后的自转速度(弧度/秒,约等于旧地球的速度) + + // 准备缓动函数(飞入时用 easeOutCubic,让粒子接近球面时减速) + function easeOutCubic(t) { + return 1 - Math.pow(1 - t, 3); } - // 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); + // 准备粒子数据:startPositions / targetPositions / current positions + var geometry = new THREE.BufferGeometry(); + var positions = new Float32Array(PARTICLE_COUNT * 3); + var startPositions = new Float32Array(PARTICLE_COUNT * 3); + var targetPositions = new Float32Array(PARTICLE_COUNT * 3); - 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; - }); - } + // 随机在球壳表面生成点(均匀分布),作为目标位置 + function randomPointOnSphere(radius) { + // 使用均匀球面采样:theta = 2πu, phi = arccos(2v - 1) + 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 }; } - // 告诉 three-globe 当前渲染器的实际尺寸,避免默认按全窗口大小计算导致比例失真 - if (typeof globe.rendererSize === "function") { - globe.rendererSize(new THREE.Vector2(width, height)); + function randomPointFarFromCenter(minR, maxR) { + var radius = minR + Math.random() * (maxR - minR); + 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 初始化完成后,根据实际半径自动调整相机距离,保证整球落在视野内 - 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); + for (var i = 0; i < PARTICLE_COUNT; i++) { + var i3 = i * 3; - // 在陆地多边形下方放置一个略小的球体作为“海洋”,用白色突出球体轮廓 - // 颜色和透明度可以在这里调整 - 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 target = randomPointOnSphere(SPHERE_RADIUS); + targetPositions[i3] = target.x; + targetPositions[i3 + 1] = target.y; + targetPositions[i3 + 2] = target.z; - // 在节点位置放置小型静态“节点点” - var nodeGeom = new THREE.SphereGeometry(r * 0.02, 16, 16); - var nodeMat = new THREE.MeshBasicMaterial({ - color: "#38BDF8", - transparent: true, - opacity: 0.9, - }); + var start = randomPointFarFromCenter(START_RADIUS_MIN, START_RADIUS_MAX); + startPositions[i3] = start.x; + startPositions[i3 + 1] = start.y; + startPositions[i3 + 2] = start.z; - // 通知 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); - }); - }); + // 初始位置就是起始位置(粒子还未飞入) + positions[i3] = start.x; + positions[i3 + 1] = start.y; + positions[i3 + 2] = start.z; } - // 按 hollow-globe 风格加载陆地多边形,绘制空心地球轮廓 - (function loadLandPolygons() { - if (typeof topojson === "undefined") { - console.warn( - "[Globe] topojson-client is undefined, unable to render hollow globe polygons" - ); - return; - } + geometry.setAttribute( + "position", + new THREE.BufferAttribute(positions, 3) + ); - // 请确保 /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; + var material = new THREE.PointsMaterial({ + color: 0x38bdf8, // 粒子主色(可自行调整) + size: 0.06, // 每个粒子在世界中的尺寸 + sizeAttenuation: true, + transparent: true, + opacity: 0.95, + depthWrite: false, + blending: THREE.AdditiveBlending, + }); - 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 - ); - }); - })(); + var particleSphere = new THREE.Points(geometry, material); + scene.add(particleSphere); + + // 也可以增加一个非常淡的内层球体,进一步强调轮廓(如不需要可注释掉) + var innerSphereGeom = new THREE.SphereGeometry(SPHERE_RADIUS * 0.98, 48, 48); + var innerSphereMat = new THREE.MeshBasicMaterial({ + color: 0x0f172a, + transparent: true, + opacity: 0.25, + side: THREE.BackSide, + }); + var innerSphereMesh = new THREE.Mesh(innerSphereGeom, innerSphereMat); + scene.add(innerSphereMesh); // 暴露给调试用 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.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(); + // 动画循环:粒子从远处飞入,逐渐聚合成球体,并缓慢自转 + var startTime = performance.now(); + var lastTime = startTime; + function animate() { requestAnimationFrame(animate); var now = performance.now(); var delta = (now - lastTime) / 1000; + var elapsed = now - startTime; 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); }