在网页中展示加载进度,最常见的是简单的转圈或进度条。但如果想要更具科技感的表现形式,可以利用 Three.js 的 3D 渲染能力,再结合 GSAP 的时间线动画,打造一个动感的 3D 圆环加载动画(Torus Loader)。

1. 全局变量与 Canvas
console.clear();
const canvas = document.querySelector("canvas");
let scene, camera, light, renderer;-
console.clear():每次刷新页面时清空调试信息。 -
canvas:获取 HTML 中唯一的<canvas>,Three.js 会在这里绘制内容。 -
scene/camera/light/renderer:声明全局变量,方便在多个函数里访问。
2. setup:初始化场景
const setup = () => {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(-34, 3, -2);
renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: canvas
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x452afc);
light = new THREE.HemisphereLight(0xffffff, 0xb4b3f2, 1);
scene.add(light);
controls = new THREE.OrbitControls(camera, renderer.domElement);
};-
Scene:3D 世界容器。
-
PerspectiveCamera:透视相机,75° 视角,近平面 0.1,远平面 1000。初始机位在 (-34, 3, -2),稍微偏左。
-
WebGLRenderer:
-
antialias: true抗锯齿。 -
setSize设置渲染区域为全屏。 -
setPixelRatio适配高分屏。 -
setClearColor设置背景色。
-
-
HemisphereLight:半球光,模拟天空光和地面反射,能让物体被柔和照亮。
-
OrbitControls:调试时能用鼠标拖拽查看场景。
3. 定义环与枢轴
let ring1, ring2;
let pivot1 = new THREE.Group();
let pivot2 = new THREE.Group();-
ring1、ring2:两个圆环网格。 -
pivot1、pivot2:枢轴(Group),用于整体旋转或平移。这样动画时操作 pivot 就能带动环。
4. ringVert:横放的圆环
const ringVert = (x) => {
const geo = new THREE.TorusBufferGeometry(4, 1, 20, 100);
const mat = new THREE.MeshLambertMaterial({ color: 0xffffff });
ring1 = new THREE.Mesh(geo, mat);
ring1.position.x = x;
pivot1.add(ring1);
scene.add(pivot1);
pivot1.applyMatrix4(new THREE.Matrix4().makeTranslation(-6, 0, 0));
};-
TorusBufferGeometry(4, 1, 20, 100):生成半径 4、管道半径 1 的圆环。 -
MeshLambertMaterial:漫反射材质,受半球光影响。 -
ring1.position.x = x:让环偏移到 pivot 的右侧。 -
pivot1.applyMatrix4(translation):把 pivot 整体向左移 6 单位,留出空间。
5. ringHor:竖放的圆环
const ringHor = (x, rotX) => {
const geo = new THREE.TorusBufferGeometry(4, 1, 20, 100);
const mat = new THREE.MeshLambertMaterial({ color: 0xffffff });
ring2 = new THREE.Mesh(geo, mat);
ring2.position.x = x;
ring2.rotation.x = rotX;
pivot2.add(ring2);
scene.add(pivot2);
pivot2.applyMatrix4(new THREE.Matrix4().makeTranslation(-10, 0, 0));
};-
也是一个圆环,和上一个类似。
-
ring2.rotation.x = rotX:把环绕 X 轴旋转 -90°,竖起来。 -
pivot2.applyMatrix4(translation):整体向左 10 单位,和第一个错开。
6. animateRings:环的动画
const animateRings = () => {
const tl = gsap.timeline({ repeat: -1, defaults: { ease: "power2.inOut" } });
tl.to(pivot1.rotation, { y: -3.14 }, "right")
.to(pivot2.rotation, { z: 3.14 }, "up")
.add(() => {
pivot1.applyMatrix4(new THREE.Matrix4().makeTranslation(-8, 0, 0));
gsap.set(ring1.position, { x: -4 });
})
.to(pivot1.rotation, { y: -3.14 }, "left")
.add(() => {
pivot2.applyMatrix4(new THREE.Matrix4().makeTranslation(-8, 0, 0));
gsap.set(ring2.position, { x: -4 });
})
.to(pivot2.rotation, { z: 0 }, "down")
.add(() => {
pivot1.applyMatrix4(new THREE.Matrix4().makeTranslation(+8, 0, 0));
gsap.set(ring1.position, { x: 4 });
})
.add(() => {
pivot2.applyMatrix4(new THREE.Matrix4().makeTranslation(+8, 0, 0));
gsap.set(ring2.position, { x: 4 });
});
return tl.timeScale(0.575);
};-
使用 GSAP 的
timeline创建循环动画。 -
pivot1.rotation.y:第一个环绕 Y 轴旋转。 -
pivot2.rotation.z:第二个环绕 Z 轴旋转。 -
每段旋转完成后:
-
用
applyMatrix4把 pivot 瞬间移动到更左边。 -
用
gsap.set把环的局部位置复位(从右侧重新出现)。
-
-
最后再把 pivot 拉回到初始位置,形成完整循环。
-
timeScale(0.575):整体放慢速度。
7. animateCamera:相机动画
const animateCamera = () => {
const tl = gsap.timeline({ repeat: -1, defaults: { ease: "none" } });
tl.to(camera.position, {
x: "-=16",
duration: 2,
onComplete: () => {
gsap.set(camera.position, { x: "+=24" });
}
});
return tl.timeScale(0.575);
};-
让相机沿 X 方向等速移动。
-
每次向左移动 16 个单位,然后瞬间往右拉 24 单位。这样形成连续的来回运动。
-
ease: "none"保证相机恒速移动。
8. 渲染与自适应
const render = () => {
requestAnimationFrame(render);
renderer.render(scene, camera);
};
const resize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};-
render():使用requestAnimationFrame循环渲染场景。 -
resize():窗口变化时更新相机宽高比和渲染尺寸。
9. 程序入口
window.addEventListener("load", () => {
setup();
render();
ringVert(4);
ringHor(4, THREE.Math.degToRad(-90));
animateRings();
animateCamera();
});
window.addEventListener("resize", () => {
resize();
});-
页面加载完成后:
-
初始化场景。
-
开始渲染循环。
-
创建两个环。
-
启动环动画和相机动画。
-
-
监听
resize,保证窗口变化时画面不变形。
总结
这份代码的核心思想是:
-
两个 pivot 分别带动两个圆环旋转。
-
通过瞬间平移 pivot + 重置环位置,营造“无限传送带”的循环效果。
-
相机等速平移,保持画面动态感。
这样最终的效果是:两个圆环不断交替旋转,从画面中进出,形成一个具有节奏感的 3D 加载动画。
代码:https://codepen.io/kdbkapsere/pen/LYZrxEp (opens in a new tab)