diff --git a/js/globe.js b/js/globe.js index fcee1e1..30c9c50 100644 --- a/js/globe.js +++ b/js/globe.js @@ -1,6 +1,5 @@ -// 首页 3D 粒子地球(banner 右侧) -// 改造为:大量粒子从远处飞入,最终聚合成一个球体,并缓慢自转 -// 依赖:全局 THREE(在 index.html 中通过 CDN 引入) +// 3D 地球效果(首页 banner 右侧)基于 three-globe +// 依赖:全局 THREE 和 ThreeGlobe(在 index.html 中通过 CDN 引入) (function () { function initGlobe() { @@ -15,6 +14,12 @@ ); 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; @@ -22,9 +27,9 @@ // 基础 Three.js 场景 var scene = new THREE.Scene(); - var camera = new THREE.PerspectiveCamera(55, width / height, 0.1, 1000); - // 初始视角,稍微偏上俯视球体 - camera.position.set(0, 0.8, 7.5); + 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 }); @@ -40,161 +45,150 @@ 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 } + ]; - // 可调参数:你可以根据效果需求自行修改 - var PARTICLE_COUNT = 800; // 粒子数量,越多越实,但性能负担也越大 - var SPHERE_RADIUS = 3.1; // 球体半径(世界单位,调大可让球在竖直方向更“撑满”) - var SPHERE_OFFSET_X = 3.5; // 球心在 X 轴上的偏移量,用于将球体放在画布偏右的位置 - var START_RADIUS_MIN = 10.0; // 粒子起始距离下限(调大一点,让更多粒子从画布边缘外飞入) - var START_RADIUS_MAX = 18.0; // 粒子起始距离上限 - var FORMATION_DURATION = 13000; // 粒子从散开到聚合成球的时间(毫秒) - var ROTATION_SPEED = 0.25; // 成形后的自转速度(弧度/秒,约等于旧地球的速度) + // 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); - // 准备缓动函数(飞入时用 easeOutCubic,让粒子接近球面时减速) - function easeOutCubic(t) { - return 1 - Math.pow(1 - t, 3); + scene.add(globe); + + // 存放自定义“闪烁节点” mesh,便于在动画循环中更新 + var nodeGlows = []; + + // 告诉 three-globe 当前渲染器的实际尺寸,避免默认按全窗口大小计算导致比例失真 + if (typeof globe.rendererSize === "function") { + globe.rendererSize(new THREE.Vector2(width, height)); } - // 准备粒子数据:startPositions / targetPositions / current positions / size / speedFactor - 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); - var sizes = new Float32Array(PARTICLE_COUNT); - var speedFactors = new Float32Array(PARTICLE_COUNT); - - // 随机在球壳表面生成点(均匀分布),作为目标位置 - 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 }; - } - - 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 }; - } - - for (var i = 0; i < PARTICLE_COUNT; i++) { - var i3 = i * 3; - - var target = randomPointOnSphere(SPHERE_RADIUS); - targetPositions[i3] = target.x; - targetPositions[i3 + 1] = target.y; - targetPositions[i3 + 2] = target.z; - - var start = randomPointFarFromCenter(START_RADIUS_MIN, START_RADIUS_MAX); - startPositions[i3] = start.x; - startPositions[i3 + 1] = start.y; - startPositions[i3 + 2] = start.z; - - // 初始位置就是起始位置(粒子还未飞入) - positions[i3] = start.x; - positions[i3 + 1] = start.y; - positions[i3 + 2] = start.z; - - // 粒子尺寸:每个粒子随机一个基础大小,后续在顶点着色器中再做距离衰减 - // 你可以调节下方这两个数,控制整体粒子大小的分布范围 - sizes[i] = 1.0 + Math.random() * 1.5; // 1.0 ~ 2.5 之间,避免大颗粒过于“糊” - - // 每个粒子的飞行速度因子:有的快、有的慢;到达目标后就“贴”在球面上不再移动 - // 例如 0.7~1.3 之间的随机值 - speedFactors[i] = 0.7 + Math.random() * 0.6; - } - - geometry.setAttribute( - "position", - new THREE.BufferAttribute(positions, 3) - ); - geometry.setAttribute( - "size", - new THREE.BufferAttribute(sizes, 1) - ); - - // 使用自定义着色器,让粒子可以拥有不同大小 - var material = new THREE.ShaderMaterial({ - uniforms: { - uColor: { value: new THREE.Color(0x38bdf8) }, // 粒子主色 - uOpacity: { value: 1.0 }, - uSize: { value: 16.0 }, // 全局尺寸基准,越大粒子越大 - uPixelRatio: { value: Math.min(window.devicePixelRatio || 1, 2) }, - }, - vertexShader: ` - attribute float size; - uniform float uSize; - uniform float uPixelRatio; - void main() { - vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); - gl_Position = projectionMatrix * mvPosition; - // 简单的距离衰减,让远处粒子稍微小一些 - float dist = length(mvPosition.xyz); - float att = 1.0 / (0.1 + dist * 0.35); - gl_PointSize = size * uSize * uPixelRatio * att; + // 在 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); + // 通知 three-globe 当前视角,用于部分图层的内部计算 + if (typeof globe.setPointOfView === "function") { + globe.setPointOfView(camera); } - `, - fragmentShader: ` - uniform vec3 uColor; - uniform float uOpacity; - void main() { - // 将点渲染成相对清晰的圆形(中心亮,边缘略微柔和) - vec2 c = gl_PointCoord - vec2(0.5); - float d = length(c); - // 0.35 以内基本保持满色,0.35~0.5 做少量抗锯齿过渡 - float mask = smoothstep(0.5, 0.35, d); - if (mask <= 0.0) discard; - float alpha = uOpacity * mask; - gl_FragColor = vec4(uColor, alpha); - } - `, - transparent: true, - depthWrite: false, - blending: THREE.AdditiveBlending, - }); - var particleSphere = new THREE.Points(geometry, material); - scene.add(particleSphere); + // 使用 globe 的工具方法,将经纬度转换为球面上的 3D 坐标 + var hasGetCoords = typeof globe.getCoords === "function"; + nodes.forEach(function (n, idx) { + 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 innerSphereGeom = new THREE.SphereGeometry(SPHERE_RADIUS * 0.98, 48, 48); - var innerSphereMat = new THREE.MeshBasicMaterial({ - color: 0x0f172a, - transparent: true, - opacity: 0.0, // 表面颜色完全透明,只保留粒子视觉 - side: THREE.BackSide, - }); - var innerSphereMesh = new THREE.Mesh(innerSphereGeom, innerSphereMat); - scene.add(innerSphereMesh); + var glowGeom = new THREE.SphereGeometry(r * 0.03, 16, 16); + var glowMat = new THREE.MeshBasicMaterial({ + // 绿色闪烁点 + color: "#22C55E", + transparent: true, + opacity: 0.7 + }); + var glowMesh = new THREE.Mesh(glowGeom, glowMat); + glowMesh.position.set(pos.x, pos.y, pos.z); + globe.add(glowMesh); - // 将球体整体向右偏移,让球体位于画布偏右侧,避免挡住左侧文案 - particleSphere.position.x = SPHERE_OFFSET_X; - innerSphereMesh.position.x = SPHERE_OFFSET_X; + nodeGlows.push({ + mesh: glowMesh, + // 随机相位,让节点闪烁不同步 + phase: Math.random() * Math.PI * 2 + idx + }); + }); + }); + } + + // 按 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 存在 + 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: "#1D4ED8", + transparent: true, + opacity: 0.85, + 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.__particleGlobe = { - scene: scene, - camera: camera, - renderer: renderer, - particleSphere: particleSphere, - }; + window.__globe = globe; } // 自适应窗口大小 @@ -205,56 +199,32 @@ camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); - // 同步像素比,避免在缩放/视网膜屏下粒子尺寸异常 - if (material && material.uniforms && material.uniforms.uPixelRatio) { - material.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio || 1, 2); + // 同步 three-globe 对 renderer 尺寸的感知,保证缩放比例一致 + if (typeof globe.rendererSize === "function") { + globe.rendererSize(new THREE.Vector2(w, h)); } } window.addEventListener("resize", onResize); - // 动画循环:粒子从远处飞入,逐渐聚合成球体,并缓慢自转 - var startTime = performance.now(); - var lastTime = startTime; - + // 动画循环 + var lastTime = performance.now(); function animate() { requestAnimationFrame(animate); var now = performance.now(); var delta = (now - lastTime) / 1000; - var elapsed = now - startTime; lastTime = now; - // 粒子从起始位置插值到球面目标位置 - var t = Math.min(1, elapsed / FORMATION_DURATION); - var eased = easeOutCubic(t); + // 地球自转 + globe.rotation.y += delta * 0.25; - 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]; - - // 为了让每个粒子在到达球面后“贴”在球体上,而不是等所有粒子一起完成: - // 这里为每个粒子引入独立的速度因子,使其进度各不相同,并对每个粒子单独 clamp 到 1。 - var pf = Math.min(1, eased * speedFactors[i]); // 每个粒子的独立进度 0~1 - - positions[i3] = sx + (tx - sx) * pf; - positions[i3 + 1] = sy + (ty - sy) * pf; - positions[i3 + 2] = sz + (tz - sz) * pf; - } - geometry.attributes.position.needsUpdate = true; - - // 粒子飞入过程中不旋转整球,避免“绕球旋转”的路径感, - // 仅在完全成型后才开始整体自转,形成更明显的“射线飞入 → 自转展示”的节奏 - if (t >= 1.0) { - particleSphere.rotation.y += ROTATION_SPEED * delta; - innerSphereMesh.rotation.y += ROTATION_SPEED * delta * 0.9; - } + // 节点“呼吸式”闪烁 + var t = now / 1000; + nodeGlows.forEach(function (n) { + var s = 0.7 + 0.3 * Math.sin(t * 3 + n.phase); + n.mesh.scale.set(s, s, s); + n.mesh.material.opacity = 0.4 + 0.4 * Math.sin(t * 3 + n.phase); + }); renderer.render(scene, camera); } @@ -267,4 +237,4 @@ } else { initGlobe(); } -})(); +})();*** End Patch ***!