Cyber Leap 2026 - 数字动画的技术实现

2025-12-04 React 动画 Framer Motion

概述

Cyber Leap 2026 是一个基于 React 19 + TypeScript 开发的全屏数字动画展示项目。项目通过流畅的动画序列,展示了数字从 2025 到 2026 的变换过程,配合科技感背景、粒子效果和烟花庆祝,打造了一个充满仪式感的跨年动画。本文深入解析项目的核心技术实现,包括状态机管理、Framer Motion 动画编排、Three.js 粒子系统和 Canvas 烟花效果。

核心挑战:如何通过状态机精确控制复杂的动画序列,同时保持流畅的视觉效果和良好的性能。

适用场景:

  • 跨年倒计时展示
  • 年度总结开场动画
  • 活动现场大屏展示
  • 创意数字动画演示

效果展示

启动阶段
启动阶段 - 初始动画
数字构建
数字构建 - 方块网格
完成状态
完成状态 - 2026

技术架构

技术组件 功能描述 在本项目中的作用
React 19 前端框架 组件化开发,状态管理
TypeScript 类型系统 类型安全,减少运行时错误
Framer Motion 动画库 声明式动画,流畅的过渡效果
Three.js 3D 渲染库 3D 粒子背景,WebGL 渲染
Canvas API 2D 绘图 API 烟花效果绘制
Vite 构建工具 快速开发构建,HMR 支持

系统架构

graph TD A[App 主组件] --> B[TechBackground
科技背景] 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]);

动画流程

启动阶段
数字构建(2-0-2-5)
形态变换(5→6)
庆祝效果
完成

项目通过状态机精确控制动画序列,每个阶段都有明确的持续时间和过渡逻辑:

  • 启动阶段(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 自动使用 transformopacity 属性,触发 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

自定义照片素材

  1. 准备照片素材(支持 png/jpg/jpeg/webp/avif/gif)
  2. 按数字位置分类放入对应文件夹:
    • assets/photos/0/:第 1 位数字
    • assets/photos/1/:第 2 位数字
    • assets/photos/2/:第 3 位数字
    • assets/photos/3/:第 4 位数字
  3. 准备 2 张补位照片放入 assets/morph/
  4. 重新构建项目

调整动画时间

修改 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 场景,提供沉浸式体验

标签