这不最近我司设计师又给我整了点活,实现一个加载进度的动画,刚开始我还以为放两个圆圈转一下就可以了;但是设计说我们是一个3D项目,得搞点高端大气的,于是就有了这篇文章。
环境准备
首先我们还是将三维场景的四个要素准备好,这里笔者为了简化代码,使用了自己封装的工具来创建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export default class Index { scene: Scene; camera: PerspectiveCamera; renderer: WebGLRenderer; controls: OrbitControls; constructor() { this.scene = initScene(); this.camera = initCamera(new Vector3(60, 50, 70), 55, 0.01, 200); this.renderer = initRenderer({ antialias: true, alpha: true }); this.renderer.setClearColor(0x000000, 0); this.controls = initOrbitControls(this.camera, this.renderer, false); } }
|
这里有一个我们可能会不太常见的函数:renderer.setClearColor
,它的主要作用是用于设置渲染器的清除颜色,也就是场景中的背景色;这个函数接收两个参数,第一个是一个十六进制的颜色,默认是0x000000,即黑色;第二个参数是其透明度alpha值;我们后期可以在背景中插入一些预加载图等,因此可以通过设置这个函数的alpha为0来清除背景的颜色。
环境搭建好了,神说要有光;于是我们创建两种光,环境光和平行光:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| export default class Index { initLight() { { this.ambientLight = new AmbientLight(0xffffff, 1) this.ambientLight.castShadow = false this.scene.add(this.ambientLight) }
{ this.directionalLight = new DirectionalLight(0xffffff, 1.2) this.directionalLight.position.set(0, 10, 0) this.directionalLight.castShadow = true this.scene.add(this.directionalLight) } } }
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
接着,就需要我们最重要的物体,三个方块了,我们定义一个函数来批量地创建方块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export default class Index { createCube(x: number, y: number, z: number, size: number, color: number): Mesh<BufferGeometry, MeshPhongMaterial> { const material = new MeshPhongMaterial({ color, }) const mesh = new Mesh(new BoxGeometry(size, size, size), material) mesh.castShadow = true mesh.receiveShadow = true mesh.position.set(x, y, z) mesh.name = "cube"
this.scene.add(mesh) return mesh } }
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
然后按照初始化的位置创建三个方块:
| const SIZE = 4.8 export default class Index { cube1: Mesh<BufferGeometry, MeshPhongMaterial>; cube2: Mesh<BufferGeometry, MeshPhongMaterial>; cube3: Mesh<BufferGeometry, MeshPhongMaterial>; constructor() { this.cube1 = this.createCube(0, 0, 0, SIZE, 0xf0fafc) this.cube2 = this.createCube(0, 0, SIZE, SIZE, 0xf0fafc) this.cube3 = this.createCube(0 - SIZE, 0, SIZE, SIZE, 0xf0fafc) } }
|
这里我们定义了一个全局的SIZE
变量,用于定义方块的尺寸,后续方块的位置更新也都会使用这个尺寸变量。
从代码上可能很难看出来,实际上方块按照这样的位置进行排布了:

翻滚把!方块
三个方块出场了,首先我们就来看一下如何先让一个方块向前翻滚,向前翻滚其实包含了两个动作,一个是前进,一个是旋转;因此这里还是引入我们常用的GSAP库,我们让cube3先沿着-Z轴的方向运动看一下效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import gsap from "gsap"; export default class Index { constructor() { const tl = gsap.timeline(); tl.to(this.cube3.position, { z: 0, duration: 1, }) .to( this.cube3.rotation, { x: -Math.PI / 2, duration: 1, }, "<" ); } }
|
上面代码中,我们创建了时间线,然后对Cube的position位置和rotation旋转角度进行了动画设置;由于两个动作是同时进行的,在这里我们使用了gsap的<
参数,将rotation插入到position动画执行前,表示两个动作之间的时间间隔为0秒;我们允许代码,就能看到方块翻滚运动起来了:

但是如果每个方块运动我们都写一遍这样的代码,比较费时费力;我们发现方块不是沿着X轴运动就是沿着Z轴运动,因此我们可以将方块的移动代码封装到一个函数中去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| const DURATION = 0.8;
rollCube( cube: Mesh, direction: "x" | "z", pos: number, rotation: number, rotationAxes: "x" | "y" | "z" ) { const tl = gsap.timeline(); if (direction === "x") { tl.to(cube.position, { x: pos, duration: DURATION, }).to( cube.rotation, { [rotationAxes]: rotation, duration: DURATION, }, "<" ); } else if (direction === "z") { tl.to(cube.position, { z: pos, duration: DURATION, }).to( cube.rotation, { [rotationAxes]: rotation, duration: DURATION, }, "<" ); } return tl; }
|
我们封装一个rollCube
函数,它定义了一个timeline时间线,然后根据传入的方向、位置、旋转角度、旋转轴等参数,将方块的移动和旋转进行设置;对方块的单次运动方式实现完毕之后,那我们下面就要来看下如何让他们一起运动起来呢?我们如何将单次运动的时间线进行串起来?
这里我们使用gsap的一个小技巧:嵌套时间线
;嵌套时间线的方式可以改变我们代码的组织逻辑,让我们实现各种复杂的动画逻辑,同时保持代码的简洁和可读性;那么嵌套时间线如何来做呢?
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
由于rollCube中定义了一个一个小的时间线,因此,我们可以在构造函数中定义一个主时间线,然后通过add
方法将rollCube返回的小时间线添加到主时间线中:
| export default class Index { mainTimeline: gsap.core.Timeline; constructor() { this.mainTimeline = gsap.timeline({ repeat: -1 }); } }
|
这里我们设置repeat: -1
,表示这个主时间线会无限重复执行,这样我们对方块移动只需要进行四次,就可以实现效果了,而不需要将所有方块移动到原来的位置;下面我们就可以把每个小方块的运动添加到主时间线中:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const ROTATION_UNIT = Math.PI / 2; export default class Index { mainTimeline: gsap.core.Timeline; constructor() { this.mainTimeline.add( this.rollCube(this.cube3, "z", 0, -ROTATION_UNIT, "x") ); this.mainTimeline.add( this.rollCube(this.cube2, "x", -SIZE, ROTATION_UNIT, "z") ); this.mainTimeline.add( this.rollCube(this.cube1, "z", SIZE, ROTATION_UNIT, "x") ); this.mainTimeline.add( this.rollCube(this.cube3, "x", 0, ROTATION_UNIT, "y") ); } }
|
我们通过一张图来更好的理解这个运动过程:

我们看下最后的实现效果:
本文所有源码敬请关注公众号【前端壹读】,后台回复关键词【3D方块加载动画】即可获取。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
总结
本文我们实现了一个有趣的3D方块加载动画的效果,相比于2D加载效果,3D效果通过空间运动和旋转带来了更加丰富的视觉体验。我们学习了通过setClearColor
方法设置透明背景,以及通过gsap嵌套时间线
的方式将多个方块动画串联起来,形成无限循环的加载动画效果,这种3D动画的实现方式不仅代码结构清晰,而且通过空间运动为加载效果增添了更多趣味性。