得瑟得瑟
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
yiqiu
2025-11-22 19:17:19 +08:00
parent 5dd0d850dc
commit 28d2efb4f2

View File

@@ -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 ***!