Cyber Leap 2026 - 数字动画的技术实现
概述
Cyber Leap 2026 是一个基于 React 19 + TypeScript 开发的全屏数字动画展示项目。项目通过流畅的动画序列,展示了数字从 2025 到 2026 的变换过程,配合科技感背景、粒子效果和烟花庆祝,打造了一个充满仪式感的跨年动画。本文深入解析项目的核心技术实现,包括状态机管理、Framer Motion 动画编排、Three.js 粒子系统和 Canvas 烟花效果。
适用场景:
- 跨年倒计时展示
- 年度总结开场动画
- 活动现场大屏展示
- 创意数字动画演示
效果展示
技术架构
| 技术组件 | 功能描述 | 在本项目中的作用 |
|---|---|---|
| React 19 | 前端框架 | 组件化开发,状态管理 |
| TypeScript | 类型系统 | 类型安全,减少运行时错误 |
| Framer Motion | 动画库 | 声明式动画,流畅的过渡效果 |
| Three.js | 3D 渲染库 | 3D 粒子背景,WebGL 渲染 |
| Canvas API | 2D 绘图 API | 烟花效果绘制 |
| Vite | 构建工具 | 快速开发构建,HMR 支持 |
系统架构
科技背景] A --> C[DigitDisplay
数字展示] A --> D[ThreeParticlesBackdrop
3D粒子背景] A --> E[FireworksCanvas
烟花效果] C --> C1[15个方块网格] C --> C2[数字形态变换] C --> C3[照片素材加载] D --> D1[Three.js 场景] D --> D2[粒子系统] D --> D3[动画循环] E --> E1[Canvas 绘制] E --> E2[烟花粒子] E --> E3[爆炸效果] A --> F[useViewportSize
视口尺寸Hook] style A fill:#e1f5fe style B fill:#fff3e0 style C fill:#f3e5f5 style D fill:#e8f5e9 style E fill:#ffe0b2 style F fill:#fce4ec
功能特性
| 功能模块 | 核心能力 | 技术实现 |
|---|---|---|
| 🎯 数字动画 | 流畅的数字变换动画 | Framer Motion 动画编排 |
| 📱 响应式设计 | 适配不同屏幕尺寸 | 动态计算缩放比例 |
| 🎨 粒子背景 | 科技感粒子效果 | Three.js 3D 粒子系统 |
| ✨ 形态变换 | 5 → 6 的平滑变形 | 多阶段动画序列 |
| 🎉 烟花效果 | 庆祝模式烟花动画 | Canvas 绘制烟花粒子 |
| 🔄 循环播放 | 支持点击重新播放 | 状态机管理动画流程 |
状态机设计
AppPhase 类型定义
项目使用 TypeScript 的联合类型定义了完整的动画阶段:
type AppPhase =
| 'start' // 启动阶段
| `grid15_${DigitIndex}` // 15方块网格构建
| `scatter_${DigitIndex}` // 网格散开成数字
| 'idle_2025' // 2025 闲置状态
| 'morph_step_1' // 形态变换第1步
| 'morph_step_2' // 形态变换第2步
| 'celebrate' // 庆祝阶段
| 'complete'; // 完成阶段
这种设计的优势:
- 类型安全:TypeScript 编译时检查,避免拼写错误
- 可扩展:使用模板字符串类型,支持 4 个数字位置
- 清晰明确:每个阶段都有明确的语义
动画序列编排
通过数组定义完整的动画序列,每个阶段都有精确的持续时间:
const sequence: { phase: AppPhase; duration: number }[] = [
{ phase: 'start', duration: 3600 },
{ phase: 'grid15_0', duration: 4500 },
{ phase: 'scatter_0', duration: 3200 },
{ phase: 'grid15_1', duration: 4500 },
{ phase: 'scatter_1', duration: 3200 },
// ... 更多阶段
];
使用 useEffect 自动推进到下一阶段:
useEffect(() => {
let currentIdx = sequence.findIndex(s => s.phase === phase);
if (currentIdx !== -1 && currentIdx < sequence.length - 1) {
const timer = setTimeout(() => {
setPhase(sequence[currentIdx + 1].phase);
}, sequence[currentIdx].duration);
return () => clearTimeout(timer);
}
}, [phase]);
动画流程
项目通过状态机精确控制动画序列,每个阶段都有明确的持续时间和过渡逻辑:
- 启动阶段(start):显示初始动画,持续 3.6 秒
- 数字构建阶段:依次构建 2、0、2、5 四个数字,每个数字经历网格构建(4.5秒)→ 散开成形(3.2秒)
- 闲置状态(idle_2025):2025 数字短暂停留,持续 1.1 秒
- 形态变换阶段:数字 5 通过两步变形成为数字 6,每步持续 4.2 秒
- 庆祝阶段(celebrate):触发烟花效果、3D 粒子背景、祝福语动画,持续 1.3 秒
- 完成阶段(complete):动画结束,显示"点击重新播放"提示
Framer Motion 动画实现
声明式动画
Framer Motion 的核心优势是声明式 API,通过 animate 属性根据状态动态计算目标值:
<motion.div
animate={
phase === 'celebrate' || phase === 'complete'
? { top: '50%', y: '-50%', scale: baseScale * 1.25 }
: { top: 'calc(env(safe-area-inset-top) + 18px)', y: 0, scale: baseScale }
}
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
>
<DigitDisplay phase={phase} />
</motion.div>
这段代码实现了数字在庆祝阶段放大并移动到屏幕中央的效果。
AnimatePresence 管理进入/退出
使用 AnimatePresence 管理组件的进入和退出动画:
<AnimatePresence>
{poemVisible && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
>
<p>2025 步履不停追光行</p>
<p>2026 乘风破浪再启程</p>
</motion.div>
)}
</AnimatePresence>
缓动函数
项目统一使用 [0.16, 1, 0.3, 1] 缓动曲线,这是一个自定义的贝塞尔曲线,提供了平滑的加速和减速效果。
Three.js 粒子系统
场景初始化
ThreeParticlesBackdrop 组件使用 Three.js 创建 3D 粒子背景:
renderer = new THREE.WebGLRenderer({
canvas,
alpha: true,
antialias: false,
powerPreference: 'high-performance',
});
renderer.setClearColor(0x000000, 0);
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5));
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(60, 1, 0.1, 400);
camera.position.z = 62;
粒子生成
生成 520 个粒子,随机分布在圆柱形区域内:
const count = 520;
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
const colorA = new THREE.Color('#22d3ee'); // 青色
const colorB = new THREE.Color('#a78bfa'); // 紫色
for (let i = 0; i < count; i++) {
const i3 = i * 3;
const r = 70 * Math.sqrt(Math.random());
const theta = Math.random() * Math.PI * 2;
positions[i3] = Math.cos(theta) * r;
positions[i3 + 1] = (Math.random() - 0.5) * 65;
positions[i3 + 2] = (Math.random() - 0.5) * 120;
// 颜色渐变
tmp.copy(colorA).lerp(colorB, Math.random() * 0.9);
colors[i3] = tmp.r;
colors[i3 + 1] = tmp.g;
colors[i3 + 2] = tmp.b;
}
动画循环
在 requestAnimationFrame 循环中更新粒子位置和相机旋转:
const tick = (t: number) => {
const currentPhase = phaseRef.current;
const celebrate = currentPhase === 'celebrate' || currentPhase === 'complete';
const speed = celebrate ? 0.45 : 0.22;
// 旋转场景
points.rotation.y = t * 0.00009;
points.rotation.x = -0.12 + Math.sin(t * 0.00012) * 0.06;
// 根据阶段调整粒子属性
material.opacity = celebrate ? 0.9 : 0.62;
material.size = celebrate ? 1.6 : 1.35;
// 粒子向前移动
const posAttr = geometry.getAttribute('position');
const arr = posAttr.array;
for (let i = 2; i < arr.length; i += 3) {
const z = arr[i] + speed;
arr[i] = z > 60 ? -120 : z; // 循环
}
posAttr.needsUpdate = true;
renderer.render(scene, camera);
raf = requestAnimationFrame(tick);
};
Canvas 烟花效果
粒子数据结构
每个烟花粒子包含位置、速度、生命周期等属性:
type Particle = {
x: number; // X 坐标
y: number; // Y 坐标
vx: number; // X 方向速度
vy: number; // Y 方向速度
life: number; // 当前生命值
maxLife: number; // 最大生命值
hue: number; // 色相值
size: number; // 粒子大小
};
烟花爆发
在随机位置生成烟花爆发,每次生成 18-30 个粒子:
const spawnBurst = () => {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
const x = rand(w * 0.15, w * 0.85);
const y = rand(h * 0.1, h * 0.55);
const hue = palette[Math.floor(rand(0, palette.length))];
const count = Math.floor(rand(18, 30));
for (let i = 0; i < count; i++) {
const a = rand(0, Math.PI * 2);
const s = rand(0.9, 3.5);
particlesRef.current.push({
x, y,
vx: Math.cos(a) * s,
vy: Math.sin(a) * s - rand(0.5, 2.0),
life: 0,
maxLife: rand(55, 95),
hue: hue + rand(-12, 12),
size: rand(1.0, 2.2),
});
}
};
粒子物理模拟
在每一帧中更新粒子状态,模拟重力和速度衰减:
for (const p of particlesRef.current) {
const nx = p.x + p.vx;
const ny = p.y + p.vy;
const nvy = p.vy + 0.045; // 重力加速度
const life = p.life + 1;
// 计算透明度(生命值越低越透明)
const k = 1 - life / p.maxLife;
const alpha = Math.max(0, Math.min(1, k));
// 绘制粒子
ctx.fillStyle = `hsla(${p.hue}, 100%, 65%, ${alpha})`;
ctx.beginPath();
ctx.arc(nx, ny, p.size, 0, Math.PI * 2);
ctx.fill();
// 更新粒子状态
if (life < p.maxLife) {
next.push({
...p,
x: nx,
y: ny,
vy: nvy,
life,
vx: p.vx * 0.995, // 速度衰减
size: Math.max(0.2, p.size * 0.994) // 大小衰减
});
}
}
响应式设计
自定义 Hook
useViewportSize Hook 监听视口尺寸变化:
export const useViewportSize = () => {
const [size, setSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 360,
height: typeof window !== 'undefined' ? window.innerHeight : 640,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
window.visualViewport?.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
window.visualViewport?.removeEventListener('resize', handleResize);
};
}, []);
return size;
};
动态缩放计算
根据视口宽度动态计算缩放比例:
const baseScale = Math.min(1, Math.max(0.72, (viewport.width - 24) / 420));
这个公式确保:
- 最小缩放比例为 0.72(小屏幕)
- 最大缩放比例为 1.0(大屏幕)
- 在 420px 宽度时达到 1.0 缩放
性能优化
1. 图片预加载
使用 requestIdleCallback 在空闲时预加载图片:
const preloadImages = (urls: Array<string | undefined>) => {
const queue = urls.filter((u): u is string =>
!!u && !PRELOADED_IMAGE_URLS.has(u)
);
let idx = 0;
const work = () => {
const end = Math.min(queue.length, idx + 6);
for (; idx < end; idx++) {
const url = queue[idx]!;
const img = new Image();
img.decoding = 'async';
img.fetchPriority = 'low';
img.src = url;
if (typeof img.decode === 'function') {
img.decode().catch(() => undefined);
}
}
if (idx < queue.length) scheduleIdle(work);
};
scheduleIdle(work);
};
2. GPU 加速
Framer Motion 自动使用 transform 和 opacity 属性,触发 GPU 加速:
// 使用 transform 而不是 top/left
animate={{ x: '-50%', y: '-50%', scale: 1.25 }}
3. Three.js 优化
- PointsMaterial:使用点材质渲染粒子,减少 Draw Call
- BufferGeometry:直接操作 Float32Array,避免对象创建
- 限制像素比:
Math.min(devicePixelRatio, 1.5)避免过度渲染
4. Canvas 优化
- 粒子数量限制:最多保留 520 个粒子
- 半透明背景:使用
rgba(2, 6, 23, 0.14)实现拖尾效果 - globalCompositeOperation:使用
lighter混合模式增强发光效果
核心概念
1. 状态机模式
状态机是管理复杂动画序列的最佳实践。通过明确定义每个状态和状态之间的转换规则,可以确保动画流程的可预测性和可维护性。
2. 声明式动画
Framer Motion 的声明式 API 让动画编排变得简单直观。不需要手动管理动画的开始、结束和中间状态,只需要声明目标状态,框架会自动处理过渡。
3. 粒子系统
粒子系统是实现复杂视觉效果的常用技术。通过大量简单粒子的组合,可以创造出丰富的视觉效果,如烟花、爆炸、雨雪等。
4. 物理模拟
简单的物理模拟(重力、速度衰减)可以让动画更加真实自然。不需要复杂的物理引擎,基本的运动学公式就能实现令人信服的效果。
应用场景
这种动画技术适用于:
- 跨年倒计时:展示时间变化的仪式感
- 产品发布会:吸引眼球的开场动画
- 年度总结:数据可视化的动态展示
- 活动现场:大屏幕互动展示
- 品牌宣传:科技感十足的视觉效果
项目结构
cyber-leap-2026/
├── components/
│ ├── DigitDisplay.tsx # 数字展示核心组件
│ ├── FireworksCanvas.tsx # 烟花效果 Canvas 绘制
│ ├── ParticleBurst.tsx # 粒子爆发效果
│ ├── TechBackground.tsx # 科技感背景组件
│ ├── ThreeParticlesBackdrop.tsx # Three.js 3D 粒子背景
│ └── useViewportSize.ts # 视口尺寸监听 Hook
├── assets/
│ ├── photos/ # 照片素材目录
│ │ ├── 0/ # 第1位数字照片
│ │ ├── 1/ # 第2位数字照片
│ │ ├── 2/ # 第3位数字照片
│ │ └── 3/ # 第4位数字照片
│ ├── morph/ # 形态变换补位照片
│ └── imageSources.ts # 照片路径配置
├── App.tsx # 主应用组件
├── index.tsx # 应用入口
├── index.css # 全局样式
├── vite.config.ts # Vite 配置
└── package.json # 项目配置
使用指南
安装和运行
# 安装依赖
npm install
# 开发模式
npm run dev
# 构建生产版本
npm run build
# 预览生产版本
npm run preview
自定义照片素材
- 准备照片素材(支持 png/jpg/jpeg/webp/avif/gif)
- 按数字位置分类放入对应文件夹:
assets/photos/0/:第 1 位数字assets/photos/1/:第 2 位数字assets/photos/2/:第 3 位数字assets/photos/3/:第 4 位数字
- 准备 2 张补位照片放入
assets/morph/ - 重新构建项目
调整动画时间
修改 App.tsx 中的 sequence 数组,调整每个阶段的持续时间:
const sequence: { phase: AppPhase; duration: number }[] = [
{ phase: 'start', duration: 3600 }, // 启动阶段 3.6秒
{ phase: 'grid15_0', duration: 4500 }, // 网格构建 4.5秒
{ phase: 'scatter_0', duration: 3200 }, // 散开动画 3.2秒
// ... 根据需要调整
];
浏览器支持
项目使用了现代 Web API,建议使用以下浏览器的最新版本:
- Chrome / Edge 90+
- Firefox 88+
- Safari 14+
需要支持 WebGL(Three.js)、Canvas 2D(烟花效果)和 ES2020+ 语法。
扩展思路
可以进一步扩展此项目:
- 音效同步:添加背景音乐和音效,与动画同步
- 交互控制:支持用户控制动画播放、暂停、快进
- 主题切换:支持多种视觉主题(科技、梦幻、简约)
- 数据驱动:根据实时数据动态生成动画内容
- WebGPU:探索 WebGPU API,进一步提升渲染性能
- VR/AR:扩展到 VR/AR 场景,提供沉浸式体验