Three.js 和 GSAP 来实现 3D 翻转揭示动画
使用 Three.js (opens in a new tab) 和 GSAP(TweenMax) (opens in a new tab) 来实现一个简单的 3D 翻转揭示动画。当点击页面中的 Trigger 按钮时,屏幕上会出现很多 3D 方块并逐渐翻转、移出视野,同时使背景内容逐渐显现。
主要流程可以简单分为以下几个步骤:
- Three.js 场景初始化
- 在场景中添加灯光
- 批量创建 3D 方块并排布到屏幕覆盖的区域
- 通过 GSAP 设置动画(翻转 + 移动 + 透明度变化)
- 动画结束后切换页面状态
接下来让我们来看看代码结构,边学边理解。
二、HTML 结构
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<!-- 引入 Three.js 和 TweenMax -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/100/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script>
<style>
/* 相关的样式省略,主要控制页面布局、字体样式以及动画显示 */
</style>
</head>
<body class="text-center">
<div id="page">
<div class="cover-container d-flex h-100 p-3 mx-auto flex-column">
<header class="masthead mb-auto">
<div class="inner">
<h3 class="masthead-brand">Reveal #1</h3>
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link active" href="#">Home</a>
<a
class="nav-link"
href="https://codepen.io/soju22/"
target="_blank"
>Codepen Profile</a
>
<a
class="nav-link"
href="https://codepen.io/collection/AGZywR"
target="_blank"
>ThreeJS Collection</a
>
</nav>
</div>
</header>
<main role="main" class="inner cover">
<h1 class="cover-heading">Simple 3D Reveal Effect</h1>
<p class="lead">
This simple effect is made with ThreeJS and TweenMax.
</p>
<p class="lead">
<a href="#" id="trigger" class="btn btn-lg btn-secondary"
>Trigger</a
>
</p>
</main>
<footer class="mastfoot mt-auto">
<div class="inner">
<p></p>
</div>
</footer>
</div>
</div>
<!-- 用来做揭示效果的画布 -->
<canvas id="reveal-effect"></canvas>
<script>
// 接下来是核心的 JavaScript 逻辑
</script>
</body>
</html>
关键点解析
<canvas id="reveal-effect"></canvas>
这是 Three.js 用来渲染 3D 场景的画布。所有 3D 效果最终会绘制到这个元素上。<a href="#" id="trigger">Trigger</a>
点击这个按钮可以触发并重新播放 3D 翻转效果。
三、核心脚本
下面的脚本主要包含了一系列函数,依次完成场景初始化、灯光和对象的创建、动画效果以及浏览器窗口自适应等工作。
let renderer, scene, camera, cameraCtrl;
let width, height, cx, cy, wWidth, wHeight;
const TMath = THREE.Math;
let conf = {
color: 0xffffff,
objectWidth: 12,
objectThickness: 3,
ambientColor: 0x808080,
light1Color: 0xffffff,
shadow: false,
perspective: 75,
cameraZ: 75,
};
let objects = [];
let geometry, material;
let hMap, hMap0, nx, ny;
1. 全局变量与配置
renderer, scene, camera
:Three.js 中的核心对象,分别代表渲染器、场景和相机。width, height, cx, cy
:分别表示浏览器窗口的宽/高以及中心点。wWidth, wHeight
:渲染区域(视锥体)内可视宽度和高度。conf
:封装了一些可调整的配置,如物体宽度、高度、相机参数、灯光颜色等等。objects
:存储所有创建的 3D 方块,方便统一管理动画。geometry, material
:3D 网格(geometry)和材质(material)在创建方块时被复用。nx, ny
:横纵方向上方块的数量。
2. init()
:初始化函数
function init() {
// 创建渲染器
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("reveal-effect"),
antialias: true,
alpha: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建透视相机
camera = new THREE.PerspectiveCamera(
conf.perspective,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = conf.cameraZ;
// 创建场景
scene = new THREE.Scene();
// 创建方块的基础几何体
geometry = new THREE.BoxGeometry(
conf.objectWidth,
conf.objectWidth,
conf.objectThickness
);
// 事件监听
window.addEventListener("load", initScene);
document.getElementById("trigger").addEventListener("click", initScene);
// 开始动画循环
animate();
}
解析
new THREE.WebGLRenderer
使用 WebGL 渲染器将 3D 对象绘制到<canvas>
上。renderer.setSize(window.innerWidth, window.innerHeight)
设置渲染器大小为浏览器窗口大小。camera = new THREE.PerspectiveCamera(...)
采用透视相机(PerspectiveCamera),让场景有远近透视效果。scene = new THREE.Scene()
创建一个空的场景容器,后续的灯光、模型都要加到这个场景里。geometry = new THREE.BoxGeometry(...)
用BoxGeometry
创建一个方块的几何形状,后续会在循环里实例化多个方块。- 事件绑定
window.addEventListener("load", initScene)
:当页面加载完成后执行initScene()
。document.getElementById("trigger").addEventListener("click", initScene)
:点击按钮可以重新执行initScene()
,从而重复动画。
animate()
开始渲染循环,让 Three.js 持续更新场景。
3. initScene()
:重新初始化场景内容
function initScene() {
onResize();
scene = new THREE.Scene();
initLights();
initObjects();
}
每次触发 initScene()
时,会做以下事情:
onResize()
:根据当前浏览器窗口大小,更新渲染尺寸和相机参数。- 重新创建场景:将
scene
重置为空场景。 initLights()
:往场景中添加灯光。initObjects()
:批量创建并排布 3D 方块。
4. initLights()
:初始化灯光
function initLights() {
scene.add(new THREE.AmbientLight(conf.ambientColor));
let light = new THREE.PointLight(0xffffff);
light.position.z = 100;
scene.add(light);
}
- 环境光(AmbientLight):为整个场景提供基础照明,让物体不会一片漆黑。
- 点光源(PointLight):类似于一个灯泡,从一个点向各个方向发出光线。这里让它位于相机附近。
5. initObjects()
:批量创建方块
function initObjects() {
objects = [];
nx = Math.round(wWidth / conf.objectWidth) + 1;
ny = Math.round(wHeight / conf.objectWidth) + 1;
let mesh, x, y;
for (let i = 0; i < nx; i++) {
for (let j = 0; j < ny; j++) {
material = new THREE.MeshLambertMaterial({
color: conf.color,
transparent: true,
opacity: 1,
});
mesh = new THREE.Mesh(geometry, material);
x = -wWidth / 2 + i * conf.objectWidth;
y = -wHeight / 2 + j * conf.objectWidth;
mesh.position.set(x, y, 0);
objects.push(mesh);
scene.add(mesh);
}
}
document.body.classList.add("loaded");
startAnim();
}
nx, ny
:根据浏览器的可视宽高以及方块的大小,计算横纵两个维度放多少个方块才可以铺满画面。- 循环创建方块:
- 使用
MeshLambertMaterial
创建可投射光照的材质。 - 将每个方块的位置设到对应的坐标,铺满整个场景。
- 将方块储存在
objects
数组里,方便后续动画调用。
- 使用
document.body.classList.add("loaded")
:给<body>
加上一个类名,用于 CSS 的过渡。startAnim()
:创建完所有方块后,立即开始动画。
6. startAnim()
:启动动画
function startAnim() {
document.body.classList.remove("revealed");
objects.forEach((mesh) => {
mesh.rotation.set(0, 0, 0);
mesh.material.opacity = 1;
mesh.position.z = 0;
let delay = TMath.randFloat(1, 2);
let rx = TMath.randFloatSpread(2 * Math.PI);
let ry = TMath.randFloatSpread(2 * Math.PI);
let rz = TMath.randFloatSpread(2 * Math.PI);
// 使用 TweenMax 为每个方块添加动画
TweenMax.to(mesh.rotation, 2, { x: rx, y: ry, z: rz, delay: delay });
TweenMax.to(mesh.position, 2, {
z: 80,
delay: delay + 0.5,
ease: Power1.easeOut,
});
TweenMax.to(mesh.material, 2, { opacity: 0, delay: delay + 0.5 });
});
setTimeout(() => {
document.body.classList.add("revealed");
}, 4500);
}
- 重置每个方块的初始属性:将旋转、透明度、z 坐标都归零,以便能重新播放动画。
- 随机延迟与随机旋转:
TMath.randFloat(1, 2)
:为每个方块生成 1 ~ 2 秒不等的延迟,增加整体翻转过程的层次感。TMath.randFloatSpread(2 * Math.PI)
:在-π 到 π
的范围内随机生成旋转角度,让方块在翻转时方向各不相同。
- 方块移出视野 & 渐隐:
TweenMax.to(mesh.position, 2, { z: 80 })
:把方块往 Z 轴正方向移动一段距离。TweenMax.to(mesh.material, 2, { opacity: 0 })
:方块逐渐变得透明。
- 添加 “revealed” 类:
- 动画约在 4.5 秒后结束,给
<body>
再加上一个类,表示已经完成揭示。这时可以移除不需要的元素或让背景内容完全呈现。
- 动画约在 4.5 秒后结束,给
7. animate()
:渲染循环
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
requestAnimationFrame(animate)
:浏览器专门用于动画的方法,保证尽可能平滑。renderer.render(scene, camera)
:每帧都用相机去渲染当前场景。如果场景或相机发生改变,则能实时更新。
8. onResize()
:自适应窗口大小
function onResize() {
width = window.innerWidth;
cx = width / 2;
height = window.innerHeight;
cy = height / 2;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
size = getRendererSize();
wWidth = size[0];
wHeight = size[1];
}
- 更新相机宽高比:当浏览器的宽度或高度改变后,需要更新
camera.aspect
并调用updateProjectionMatrix()
,否则画面会变形。 - 更新渲染器大小:同步渲染器的大小以适配新的窗口尺寸。
- 记录可视宽高:
wWidth
和wHeight
用于后续铺设方块时计算网格数量。
9. getRendererSize()
:计算可视区域大小
function getRendererSize() {
const cam = new THREE.PerspectiveCamera(conf.perspective, camera.aspect);
const vFOV = (cam.fov * Math.PI) / 180;
const height = 2 * Math.tan(vFOV / 2) * Math.abs(conf.cameraZ);
const width = height * cam.aspect;
return [width, height];
}
- 通过三角函数计算摄像机所能看到的“真实宽高”。
- 这里将
conf.cameraZ
作为距离来推算,在这个距离上能看到的宽度与高度是多少。 - 最终用于决定方块在 X、Y 方向上要排列多少行列。
四、使用与效果
- 打开页面:当页面加载完毕后,
init()
会自动执行。 - 点击 Trigger 按钮:将会再次执行
initScene()
,重新生成方块并播放翻转动画。 - 动画过程:方块随机旋转后,向 Z 方向移动并逐渐消失;此时页面背景从白色逐渐过渡到深色,最后页面上内容“显现”出来。
你可以根据自己的需求,修改下列内容以实现不同效果:
objectWidth
:方块的尺寸大小。objectThickness
:方块的厚度。cameraZ
:相机初始位置,影响视觉上的远近。color
:方块的颜色。- 动画细节:比如 TweenMax 的持续时间、延迟范围、旋转范围等。
五、总结
- Three.js 场景搭建:渲染器、相机、场景是最基础的“三件套”,接着再添加对象和灯光。
- GSAP 动画:通过对方块的旋转、透明度和位置进行缓动,可以打造出丰富的动画效果。
- 事件与响应:加载时、窗口大小改变时、按钮点击时,各自触发对应的函数,让程序更具交互性。
- 布局与过渡:结合 CSS,可以更好地完成前后场景的切换。