Files
BlackFruit-UI/js/globe.js
yiqiu 5dd0d850dc
All checks were successful
continuous-integration/drone/push Build is passing
法大师傅
2025-11-22 19:14:08 +08:00

271 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在 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;
}
var width = container.clientWidth || 400;
var height = container.clientHeight || 400;
// 基础 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);
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);
// -------------------------------
// 粒子球体:从远处飞入,最终聚合成球
// -------------------------------
// 可调参数:你可以根据效果需求自行修改
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; // 成形后的自转速度(弧度/秒,约等于旧地球的速度)
// 准备缓动函数(飞入时用 easeOutCubic让粒子接近球面时减速
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
// 准备粒子数据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;
}
`,
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);
// 也可以增加一个非常淡的内层球体,进一步强调轮廓(如不需要可注释掉)
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);
// 将球体整体向右偏移,让球体位于画布偏右侧,避免挡住左侧文案
particleSphere.position.x = SPHERE_OFFSET_X;
innerSphereMesh.position.x = SPHERE_OFFSET_X;
// 暴露给调试用
if (typeof window !== "undefined") {
window.__particleGlobe = {
scene: scene,
camera: camera,
renderer: renderer,
particleSphere: particleSphere,
};
}
// 自适应窗口大小
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);
// 同步像素比,避免在缩放/视网膜屏下粒子尺寸异常
if (material && material.uniforms && material.uniforms.uPixelRatio) {
material.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio || 1, 2);
}
}
window.addEventListener("resize", onResize);
// 动画循环:粒子从远处飞入,逐渐聚合成球体,并缓慢自转
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;
// 粒子从起始位置插值到球面目标位置
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];
// 为了让每个粒子在到达球面后“贴”在球体上,而不是等所有粒子一起完成:
// 这里为每个粒子引入独立的速度因子,使其进度各不相同,并对每个粒子单独 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;
}
renderer.render(scene, camera);
}
animate();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initGlobe);
} else {
initGlobe();
}
})();