1. 年会抽奖程序的需求背景与核心痛点
每到年底,各大公司的年会筹备工作总是如火如荼。作为暖场环节的重头戏,抽奖活动往往决定着整场年会的氛围基调。传统抽奖方式通常存在几个明显痛点:
- 视觉表现单调:简单的数字滚动或名单闪现缺乏冲击力
- 参与感薄弱:观众只是被动等待结果公布
- 公平性质疑:缺乏直观的随机性展示容易引发疑虑
- 技术门槛高:要实现酷炫效果往往需要专业设备或外包开发
我去年负责公司年会技术支持时,就遇到了这样的困境。在尝试了市面上几种现成方案后,最终决定用Web技术栈自主开发一套3D球体动态抽奖系统。这个方案具有以下独特优势:
- 视觉表现力强:通过Three.js实现的3D球体可以360°展示抽奖过程
- 参与互动性高:支持全员手机扫码实时观看抽奖动画
- 开发成本低:基于Node.js+Express的后端和前端Three.js组合,全套代码不超过500行
- 数据对接灵活:可直接读取Excel员工名单,避免手动录入的繁琐
提示:选择Three.js而非2D动画库的关键在于,3D空间中的粒子运动更具随机性和观赏性,能有效增强抽奖过程的公平可信度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与架构设计
2.1 核心组件拆解
整个系统采用前后端分离架构,主要技术栈如下:
| 模块 | 技术选型 | 选型理由 |
|---|---|---|
| 3D渲染引擎 | Three.js r148 | 轻量级WebGL框架,粒子系统API完善,兼容性好 |
| 后端服务 | Express 4.18 | 路由设计简洁,中间件生态丰富,适合快速开发REST API |
| 数据存储 | Excel | 行政人员熟悉的格式,无需额外数据库维护 |
| 网络通信 | Socket.IO 4.5 | 实现实时广播抽奖状态,避免页面轮询 |
| 部署环境 | Node.js 16.x | 长期支持版本,稳定性有保障 |
2.2 关键技术创新点
这套系统的核心亮点在于3D球体的动态算法设计:
-
粒子初始化:将每个员工姓名转换为带权重的3D坐标点
javascript复制function initParticles(names) { return names.map((name, i) => { const phi = Math.acos(-1 + (2 * i) / names.length) const theta = Math.sqrt(names.length * Math.PI) * phi return { name, x: radius * Math.sin(phi) * Math.cos(theta), y: radius * Math.sin(phi) * Math.sin(theta), z: radius * Math.cos(phi) } }) } -
抽奖动画算法:采用Perlin噪声生成随机运动轨迹
javascript复制function updateParticles() { const time = Date.now() * 0.0005 particles.forEach(p => { p.x += noise.simplex3(p.x, p.y, time) * 0.5 p.z = Math.max(0, p.z - 0.01) // 向屏幕外扩散 }) } -
中奖判定逻辑:基于视锥体裁剪的空间筛选
javascript复制function getVisibleParticles(camera) { const frustum = new THREE.Frustum() frustum.setFromProjectionMatrix( new THREE.Matrix4().multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ) ) return particles.filter(p => frustum.containsPoint(new THREE.Vector3(p.x, p.y, p.z)) ) }
3. 实现步骤详解
3.1 环境准备与项目初始化
首先创建Node.js项目并安装核心依赖:
bash复制mkdir lottery-3d && cd lottery-3d
npm init -y
npm install express socket.io xlsx three simplex-noise
目录结构设计如下:
code复制/lottery-3d
├── /public # 前端静态资源
│ ├── index.html
│ ├── main.js
│ └── style.css
├── /server # 后端服务
│ ├── app.js
│ └── routes.js
├── data.xlsx # 员工名单
└── package.json
3.2 Excel数据解析模块
使用xlsx库处理员工名单:
javascript复制// server/routes.js
const XLSX = require('xlsx')
function loadEmployees() {
const workbook = XLSX.readFile('./data.xlsx')
const sheet = workbook.Sheets[workbook.SheetNames[0]]
return XLSX.utils.sheet_to_json(sheet).map(row => ({
id: row['工号'],
name: row['姓名'],
dept: row['部门']
}))
}
注意:Excel首行必须包含"工号"、"姓名"、"部门"等列标题,建议提前做好数据清洗,去除空行和特殊字符。
3.3 3D场景搭建
前端核心渲染逻辑:
javascript复制// public/main.js
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000)
const renderer = new THREE.WebGLRenderer({ antialias: true })
function init() {
// 初始化相机位置
camera.position.z = 50
// 创建粒子材质
const material = new THREE.PointsMaterial({
size: 2,
color: 0x3498db,
transparent: true,
opacity: 0.8
})
// 从服务端获取员工数据
fetch('/api/employees')
.then(res => res.json())
.then(createParticles)
}
function createParticles(employees) {
const geometry = new THREE.BufferGeometry()
const positions = new Float32Array(employees.length * 3)
employees.forEach((emp, i) => {
positions[i*3] = Math.random() * 40 - 20
positions[i*3+1] = Math.random() * 40 - 20
positions[i*3+2] = Math.random() * 40 - 20
})
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
const particles = new THREE.Points(geometry, material)
scene.add(particles)
}
3.4 抽奖流程控制
完整的抽奖状态管理:
javascript复制// server/app.js
const express = require('express')
const socketio = require('socket.io')
const app = express()
const server = app.listen(3000)
const io = socketio(server)
let lotteryState = 'idle' // idle|rolling|result
io.on('connection', socket => {
socket.emit('state-change', lotteryState)
socket.on('start-lottery', () => {
lotteryState = 'rolling'
io.emit('state-change', 'rolling')
// 10秒后自动开奖
setTimeout(() => {
const winner = pickWinner()
lotteryState = 'result'
io.emit('result', winner)
}, 10000)
})
})
function pickWinner() {
const employees = loadEmployees()
return employees[Math.floor(Math.random() * employees.length)]
}
4. 实战优化与效果增强
4.1 性能调优技巧
在大规模员工场景下(超过500人),需要特别注意:
-
粒子实例化渲染:改用InstancedMesh提升性能
javascript复制const instancedMesh = new THREE.InstancedMesh(geometry, material, count) employees.forEach((emp, i) => { const matrix = new THREE.Matrix4() matrix.setPosition(Math.random() * 40 - 20, Math.random() * 40 - 20, Math.random() * 40 - 20) instancedMesh.setMatrixAt(i, matrix) }) -
动态细节分级(LOD):根据距离调整粒子大小
javascript复制material.onBeforeCompile = shader => { shader.fragmentShader = shader.fragmentShader.replace( 'gl_PointSize = size;', `gl_PointSize = size * (50.0 / length(vViewPosition));` ) }
4.2 视觉增强方案
通过着色器实现高级效果:
-
光晕效果:在片元着色器中添加辉光计算
glsl复制void main() { float intensity = pow(0.7 - length(gl_PointCoord - 0.5), 4.0); gl_FragColor = vec4(0.3, 0.5, 1.0, 0.8) * intensity; } -
轨迹拖尾:使用累积缓冲区技术
javascript复制const trailRenderer = new THREE.WebGLRenderTarget(width, height, { stencilBuffer: false, depthBuffer: true }) function render() { renderer.setRenderTarget(trailRenderer) renderer.clear() renderer.render(scene, camera) // 混合新旧帧 composer.render() }
4.3 移动端适配要点
-
陀螺仪交互:增加设备方向控制
javascript复制window.addEventListener('deviceorientation', e => { camera.rotation.x = e.beta * 0.01 camera.rotation.y = e.alpha * 0.01 }) -
触摸事件支持:实现手势控制视角
javascript复制let touchStart = null canvas.addEventListener('touchstart', e => { touchStart = { x: e.touches[0].clientX, y: e.touches[0].clientY } }) canvas.addEventListener('touchmove', e => { if (!touchStart) return const dx = e.touches[0].clientX - touchStart.x camera.rotation.y += dx * 0.01 })
5. 部署与现场操作指南
5.1 生产环境部署
推荐使用PM2进程管理:
bash复制npm install pm2 -g
pm2 start server/app.js --name "3d-lottery"
Nginx反向代理配置示例:
nginx复制server {
listen 80;
server_name lottery.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
}
}
5.2 现场操作流程
-
前期准备:
- 提前1小时测试网络环境
- 准备备用4G热点
- 大屏幕分辨率设置为1920x1080
-
操作步骤:
- 打开控制台页面(http://localhost:3000/admin)
- 点击"导入名单"按钮上传最新Excel
- 测试抽奖动画效果
- 年会现场投影主屏幕并打开手机端访问二维码
- 主持人点击"开始抽奖"按钮
-
应急预案:
- 如遇3D渲染失败:切换到备用的2D抽奖模式
- 网络中断:使用预生成的离线动画视频
- 数据异常:手动输入中奖者工号
5.3 数据安全措施
-
名单加密:对Excel文件进行AES加密
javascript复制const crypto = require('crypto') const cipher = crypto.createCipheriv('aes-256-cbc', key, iv) let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex') encrypted += cipher.final('hex') -
操作日志:记录所有抽奖操作
javascript复制function logAction(action) { fs.appendFileSync('lottery.log', `[${new Date().toISOString()}] ${action}\n` ) }
这套3D抽奖系统在我们公司年会上的实际运行效果远超预期。现场200多名员工通过手机实时观看3D球体中名字的飞驰穿梭,当最终获奖者的名字从众多粒子中脱颖而出时,全场爆发出热烈掌声。技术团队仅用3天就完成了全部开发,而带来的活动效果提升却是传统抽奖方式难以企及的。
