通过 Three.js 和 GSAP 的结合使用,创造一个引人入胜的 3D 抽奖体验,特别是中奖结果展示环节的动画效果,使整个抽奖过程更加令人期待。

核心技术实现
一、 Three.js 3D 场景搭建
javascript
// 创建场景、相机和渲染器
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000); // 黑色背景
// 创建透视相机
camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 1000);
camera.position.z = 500;
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
container.appendChild(renderer.domElement);
// 添加灯光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
directionalLight.position.set(0, 1, 1);
scene.add(directionalLight);
在 Three.js 中,场景(Scene) 、相机(Camera) 和渲染器(Renderer) 是构建 3D 场景的三大核心组件,它们共同协作完成 3D 画面的呈现。可以类比现实世界中 "拍摄电影" 的过程来理解:
1. 场景(Scene)
-
概念:场景是 3D 物体、灯光、相机等所有元素的 "容器",相当于现实世界中的 "拍摄现场"。
-
作用:
- 组织和管理所有 3D 对象(如网格模型、粒子系统等)。
- 定义物体之间的层级关系(父子关系)。
- 决定哪些物体对光线有反应(受光照影响)。
js
import * as THREE from 'three';
const scene = new THREE.Scene(); // 创建场景
// 向场景中添加物体(如一个立方体)
const geometry = new THREE.BoxGeometry(1, 1, 1); // 几何体
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); // 材质
const cube = new THREE.Mesh(geometry, material); // 网格模型(几何体+材质)
scene.add(cube); // 将模型添加到场景
2. 相机(Camera)
-
概念:相机决定了从哪个角度和视角 "观察" 场景中的物体,相当于现实世界中的 "摄像机"。
-
作用:
- 定义观察点(位置)和观察方向。
- 控制视野范围(类似相机的镜头焦距)。
- 决定 3D 场景如何投影到 2D 屏幕上(透视投影 / 正交投影)。
常见类型:
- 透视相机(PerspectiveCamera) :模拟人眼视角,近大远小,有真实的深度感(适合大多数 3D 场景)。
js
// 参数:视野角度(fov)、宽高比(aspect)、近裁剪面(near)、远裁剪面(far)
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5; // 相机位置(沿 z 轴远离原点)
-
正交相机(OrthographicCamera) :物体大小不受距离影响,无透视效果(适合 2D 游戏、工程绘图等)。
-
核心参数:
position
:相机在 3D 空间中的位置(x, y, z)。lookAt(target)
:设置相机的观察目标(如指向场景原点scene.position
)。
3. 渲染器(Renderer)
-
概念:渲染器是将 "场景" 通过 "相机" 的视角渲染到屏幕上的工具,相当于现实世界中的 "显示器" 或 "胶片"。
-
作用:
- 将 3D 场景转换为 2D 像素图像并绘制到
<canvas>
元素上。 - 处理光照、阴影、材质等视觉效果的计算。
- 控制渲染性能(如帧率、抗锯齿等)。
- 将 3D 场景转换为 2D 像素图像并绘制到
js
// 创建渲染器,绑定到一个 canvas 元素(或自动创建)
const renderer = new THREE.WebGLRenderer({ antialias: true }); // 开启抗锯齿
renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染尺寸(全屏)
document.body.appendChild(renderer.domElement); // 将渲染器的 canvas 添加到页面
// 渲染场景(需要在动画循环中持续调用)
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera); // 核心:用相机拍摄场景并渲染
}
animate();
三者关系总结
- 场景:存放所有 3D 物体和环境元素。
- 相机:定义 "看哪里" 和 "怎么看"。
- 渲染器:执行 "拍摄" 动作,将相机看到的场景绘制到屏幕上。
用一句话概括:渲染器将相机所 "看到" 的场景内容,实时绘制到网页的 canvas 上,形成我们最终看到的 3D 画面。这三者是 Three.js 所有 3D 应用的基础,缺一不可。
二、创建参与者头像和名称对象
javascript
// 为每个参与者创建平面几何体,并将头像作为纹理
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide
});
const geometry = new THREE.PlaneGeometry(60, 60);
const nameObject = new THREE.Mesh(geometry, material);
// 均匀分布在球面上
const phi = Math.acos(-1 + (2 * index) / participants.length);
const theta = Math.sqrt(participants.length * Math.PI) * phi;
nameObject.position.x = radius * Math.sin(phi) * Math.cos(theta);
nameObject.position.y = radius * Math.sin(phi) * Math.sin(theta);
nameObject.position.z = radius * Math.cos(phi);
// 让对象始终面向相机
nameObject.lookAt(camera.position);
三、使用 Canvas 创建头像纹理
javascript
// 使用 Canvas 创建参与者头像纹理
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// 绘制圆形头像
context.save();
context.beginPath();
context.arc(128, 100, 80, 0, Math.PI * 2);
context.closePath();
context.clip();
context.drawImage(img, 48, 20, 160, 160);
// 绘制名称
context.fillStyle = '#ffffff';
context.font = 'bold 24px Arial';
context.textAlign = 'center';
context.fillText(participant.name, 128, 220);
// 创建纹理
const texture = new THREE.CanvasTexture(canvas);
四、动画循环与场景旋转
javascript
// 动画循环
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
if (props.isSpinning) {
// 旋转整个场景
scene.rotation.y += rotationSpeed.value;
scene.rotation.x += rotationSpeed.value / 2;
}
// 中奖者卡片旋转
if (isHighlightingWinner.value && winnerObject.value) {
winnerObject.value.rotation.y += 0.03;
}
renderer.render(scene, camera);
};
// 抽奖停止时的减速效果
const slowDown = () => {
rotationSpeed.value *= 0.95;
if (rotationSpeed.value > 0.001) {
setTimeout(slowDown, 100);
} else {
rotationSpeed.value = 0;
// 选择并突出显示获奖者
const winner = selectWinner();
highlightWinner(winner);
}
};
五、使用 GSAP 实现中奖者突出效果
javascript
// 重置场景旋转
gsap.to(scene.rotation, {
x: 0,
y: 0,
z: 0,
duration: 1.0,
ease: "power2.out"
});
// 淡出其他参与者
gsap.to(obj.material, {
opacity: 0,
duration: 1.0,
onStart: () => {
obj.material.transparent = true;
}
});
// 将中奖者移至中央并放大
gsap.to(winnerObj.position, {
x: 0,
y: 0,
z: 200,
duration: 1.5,
ease: "power2.out"
});
gsap.to(winnerObj.scale, {
x: 2.5,
y: 2.5,
z: 2.5,
duration: 1.5,
ease: "power2.out"
});
六、光晕效果实现
javascript
// 创建光晕效果
const glowGeometry = new THREE.PlaneGeometry(70, 70);
const glowMaterial = new THREE.MeshBasicMaterial({
color: 0xffff00,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide
});
glowMesh = new THREE.Mesh(glowGeometry, glowMaterial);
glowMesh.position.z = obj.position.z - 1; // 放在对象后面
// 创建光晕动画
gsap.to(glowMaterial, {
opacity: 0.7,
duration: 1,
repeat: -1,
yoyo: true,
ease: "sine.inOut"
});
七、场景重置与清理
javascript
// 停止所有GSAP动画
gsap.killTweensOf(scene.rotation);
nameObjects.forEach(obj => {
gsap.killTweensOf(obj.position);
gsap.killTweensOf(obj.scale);
gsap.killTweensOf(obj.material);
});
// 清除光晕对象
if (glowMesh && scene) {
scene.remove(glowMesh);
glowMesh = null;
}
// 重新创建所有对象
createNameObjects();
实现要点与技巧
-
空间分布算法:使用球面坐标系均匀分布参与者对象,确保无论参与人数多少都能均匀分布。
-
头像加载容错:实现了多级容错机制,当远程头像加载失败时,会自动创建带有首字母的默认头像。
-
动画过渡:使用 GSAP 实现平滑的动画过渡,包括旋转减速、对象移动和透明度变化。
-
性能优化:
- 使用
requestAnimationFrame
进行动画循环 - 在组件卸载时清理资源和事件监听
- 使用
killTweensOf
停止不需要的动画
- 使用
-
响应式设计:监听窗口大小变化,动态调整渲染尺寸。
-
中奖者突出效果:
- 场景旋转重置到正面
- 其他参与者淡出并远离
- 中奖者移至中央并放大
- 添加光晕效果和旋转动画
- 显示中奖者名称
优势与特点
- 视觉吸引力:3D 效果比传统 2D 抽奖更具视觉冲击力
- 沉浸式体验:通过空间分布和动画效果增强用户体验
- 可定制性:可以轻松调整颜色、大小、动画速度等参数
- 性能良好:使用 Three.js 硬件加速渲染,即使大量参与者也能保持流畅