259 lines
9.3 KiB
JavaScript
259 lines
9.3 KiB
JavaScript
// 首页 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
|
||
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);
|
||
|
||
// 随机在球壳表面生成点(均匀分布),作为目标位置
|
||
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 之间,避免大颗粒过于“糊”
|
||
}
|
||
|
||
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];
|
||
|
||
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);
|
||
}
|
||
|
||
animate();
|
||
}
|
||
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", initGlobe);
|
||
} else {
|
||
initGlobe();
|
||
}
|
||
})();
|