开箱流水加载动画

说在前面

今天来实现一个 开箱流水线 Loading 动画 ------ 四个 3D 盒子依次"开箱",盖子翻开、墙壁展开、图标弹出,步骤之间用连接线串联,底部还有进度条同步推进。

在线体验

codepen

在线链接:codepen.io/yongtaozhen...

码上掘金

在线链接:code.juejin.cn/pen/7650046...

实现思路

整个效果可以拆成这几个关键步骤:

  1. 用 CSS 3D 搭建盒子 ------ 底面 + 四面墙 + 盖子,组成一个真实的立方体
  2. 设计铰链结构 ------ 盖子挂在背面墙上,翻盖动画才符合物理直觉
  3. 流水线时间轴 ------ 四个盒子交错触发,形成依次开箱的节奏感
  4. 缓动函数打磨 ------ 盖子翻转用余弦缓动,墙壁展开加过冲回弹,图标弹出带弹性
  5. 连接线和进度条 ------ 把各步骤串联成一条完整的流水线

核心代码

(1)3D 盒子的 DOM 结构

每个盒子由一个 scene(提供透视)> camera(固定视角)> box(3D 容器)三层嵌套,里面放 6 个面:

html 复制代码
<div class="step-scene">       <!-- perspective: 600px -->
  <div class="step-camera">    <!-- rotateX(-25deg) rotateY(30deg) 固定俯视角 -->
    <div class="box">          <!-- transform-style: preserve-3d -->
      <!-- 底面 floor -->
      <!-- 前墙 front -->
      <!-- 后墙 back → 盖子 lid 挂在这里 -->
      <!-- 左墙 left -->
      <!-- 右墙 right -->
      <!-- 地面阴影 shadow -->
    </div>
  </div>
</div>

关键 CSS 就三行:

css 复制代码
.step-scene {
  perspective: 600px;  /* 给子元素提供 3D 透视 */
}

.step-camera {
  transform-style: preserve-3d;  /* 保留子元素的 3D 变换 */
  transform: rotateX(-25deg) rotateY(30deg);  /* 固定的俯视角度 */
}

.box {
  transform-style: preserve-3d;  /* 盒子内部也要保持 3D */
}

perspective: 600px 决定了"镜头离盒子多远",值越小透视感越强。rotateX(-25deg) rotateY(30deg) 则给了一个略微俯视的等距视角,让盒子看起来立体感刚好 👀。

(2)用 JS 动态创建盒子的 6 个面

每个面的定位都靠 CSS transform 来"掰"到正确位置:

javascript 复制代码
const SIZE = 50; // 盒子尺寸

function createBoxElement(size) {
  const box = createElement("box");

  // 底面 ------ 水平放倒
  const floor = createFace(size);
  floor.style.transform = "rotateX(90deg)";
  box.appendChild(floor);

  // 四面墙 ------ 各自旋转到位
  const front = createWall(`translateZ(${size/2}px)`);
  const back  = createWall(`translateZ(${-size/2}px) rotateY(180deg)`);
  const left  = createWall(`translateX(${-size/2}px) rotateY(-90deg)`);
  const right = createWall(`translateX(${size/2}px) rotateY(90deg)`);

  // 盖子挂在背面墙上 ------ 铰链结构的关键!
  const lid = createFace(size);
  lid.style.transformOrigin = "center bottom"; // 旋转轴在底边
  back.appendChild(lid);

  return { el: box, floor, lid, walls: [front, back, left, right] };
}

这里最巧妙的是 back.appendChild(lid) ------ 盖子作为背面墙的子元素,旋转轴设在底边(transformOrigin: center bottom)。这样当我们用 rotateX 旋转盖子时,它就像真的被铰链连着一样翻开,而不是凭空旋转。

(3)三种缓动函数让动画有"质感"

这是整个效果的灵魂部分 🎨。同样是"从 A 到 B",不同的缓动曲线会带来完全不同的感觉:

盖子翻开 ------ 余弦缓动:

javascript 复制代码
function lidEase(t) {
  return (1 - Math.cos(t * Math.PI)) / 2;
}

开头慢、中间快、收尾慢,就像你掀盒子盖,先使点劲、中间自然甩开、最后缓缓到位 📦。

墙壁展开 ------ 立方缓出 + 正弦过冲:

javascript 复制代码
const t = 1 - wallProgress;
const eased = 1 - t * t * t;   // 立方缓出,越到后面越慢
const overshoot = eased + 0.08 * Math.sin(wallProgress * Math.PI);  // 加一点回弹
const wallAngle = -90 * overshoot;

墙壁展开不是匀速"倒下去"的,而是快速展开后微微"弹过头"再回来。就像你用力掰开一个纸盒,边缘会回弹一下。那个 0.08 * Math.sin(...) 就是回弹的幅度,虽然只有 8%,但视觉上让动画一下子就"活"了 ✨。

图标弹出 ------ 三次方进出:

javascript 复制代码
const popT = popRaw < 0.5
  ? 4 * popRaw * popRaw * popRaw
  : 1 - Math.pow(-2 * popRaw + 2, 3) / 2;

图标从盒子里弹出来,先慢后快再慢,像从弹簧上弹起。弹出后还加了个微浮动效果:

javascript 复制代码
const popFloat = progress >= 1 ? Math.sin(Date.now() * 0.003) * 2 : 0;

图标到位后不是僵硬地停住,而是微微上下浮动,就像悬浮在空中一样 🫧。

(4)流水线时间轴编排

四个盒子不是同时开的,而是依次触发,这就需要计算每个步骤的"出场时间":

javascript 复制代码
const STEP_DURATION = 0.2;  // 每个步骤占总时长的 20%
const gap = 0.05;           // 步骤间的间隔

// 计算交错间距
const available = 1 - STEP_DURATION - gap * (n - 1);
const stagger = available / Math.max(n - 1, 1);

for (let i = 0; i < n; i++) {
  const stepStart = i * (stagger + gap);  // 每个步骤的起始时间点
  const stepProgress = clamp((totalProgress - stepStart) / STEP_DURATION);
  updateBox(steps[i], stepProgress, hue);
}

核心思想就是:把 0~1 的总进度切成几段,每段分配给一个步骤。stagger 控制步骤之间的间距------间距大了像排队,间距小了像一窝蜂。当前的配置让四个盒子依次触发,略有重叠,形成"流水线"的感觉 🏭。

(5)动态着色 ------ 颜色跟着进度走

每个盒子有不同的色相,而且颜色随开箱进度逐渐变化:

javascript 复制代码
const hue = 210 + i * 30;  // 四个盒子色相:210, 240, 270, 300

function styleFace(face, hue, alpha, progress) {
  const sat = 40 + progress * 25;    // 饱和度 40% → 65%
  const light = 96 - progress * 12;  // 亮度 96% → 84%
  face.style.background = `hsla(${hue}, ${sat}%, ${light}%, ${0.92 * alpha})`;
}

刚开始盒子是浅浅的、几乎透明的,随着开箱逐渐变得饱和、实在。这种"渐显"的效果比一开始就实打实的好看很多 🎨。

(6)连接线填充

步骤之间的连接线不是突然出现的,而是从左向右"流过去"的:

javascript 复制代码
if (i < n - 1 && steps[i].connectorFill) {
  const connStart = stepStart + STEP_DURATION * 0.5;  // 前一步完成一半时开始
  const nextStart = (i + 1) * (stagger + gap);        // 下一步开始时结束
  const connProgress = clamp((totalProgress - connStart) / (nextStart - connStart));
  steps[i].connectorFill.style.transform = `scaleX(${connProgress})`;
}

连接线在前一个盒子开到一半时就开始填充,到下一个盒子开始时恰好填满。时间卡得刚好,让整个流水线衔接得很顺畅 🔗。

逐步拆解

整理一下整个动画的层次关系:

arduino 复制代码
wrapper
  ├── timeline(时间轴容器,flex 水平排列)
  │     ├── step[0](connect)
  │     │     ├── step-scene(透视容器,perspective: 600px)
  │     │     │     └── step-camera(3D 视角)
  │     │     │           ├── box(3D 盒子:floor + 4 walls + lid + shadow)
  │     │     │           └── pop-icon(弹出图标 SVG)
  │     │     ├── step-dot(状态圆点)
  │     │     └── step-label(步骤名称)
  │     ├── connector[0](连接线 + 填充动画)
  │     ├── step[1](verify)
  │     ├── connector[1]
  │     ├── step[2](load)
  │     ├── connector[2]
  │     └── step[3](render)
  ├── progress-bar(总进度条)
  └── progress-text(百分比文字)

动画循环机制也很简单,用 requestAnimationFrame + 取模:

javascript 复制代码
const CYCLE = 8000;   // 动画阶段 8 秒
const PAUSE = 3000;   // 暂停阶段 3 秒
const TOTAL = 11000;  // 总周期

function animate(now) {
  requestAnimationFrame(animate);
  const elapsed = now % TOTAL;              // 取模 → 无缝循环
  const totalProgress = Math.min(elapsed / CYCLE, 1);  // 8 秒内 0→1,之后保持 1
  // ...更新所有盒子和进度条
}

适用场景

这个 Loading 适合以下几种场景:

  • 应用初始化:connect → verify → load → render 正好对应 "连接服务 → 鉴权 → 加载数据 → 渲染页面" 的真实流程
  • 多步骤表单提交:每个盒子对应一个处理步骤,让用户知道"到哪一步了"
  • 文件上传/处理:上传 → 校验 → 压缩 → 完成

源码地址

gitee

源码地址:gitee.com/zheng_yongt...

GitHub

源码地址:github.com/yongtaozhen...


🌟 觉得有帮助的可以点个 star~

🖊 有什么问题或错误可以指出,欢迎 pr~

📬 有什么想要实现的功能或想法可以联系我~


公众号

关注公众号『 前端也能这么有趣 』,获取更多有趣内容。

发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。

相关推荐
RANxy1 小时前
AntV 入门系列:G6 图可视化实战
前端
尽欢i1 小时前
Vue3 customRef 封神教程:防抖、本地存储、自动埋点一套搞定,模板干干净净
前端·javascript·vue.js
VOLUN1 小时前
TypeScript封装通用RESTful BaseAPI,后台接口代码精简80%
前端·javascript
胡永双1 小时前
Hexo + GitHub Pages搭建个人Blog教程(三)
前端
hunterandroid1 小时前
[Android 从零到一] 权限管理:运行时权限与最佳实践
前端
kyrie281 小时前
Redux 完整基础操作(原生 Redux,不结合 React-Redux)
前端
因_崔斯汀1 小时前
Vue 模板编译:HTML 是怎么变成 JS 的?
前端·vue.js
UXbot1 小时前
帮助企业低门槛开展AI应用开发的平台推荐
前端·低代码·ui·交互·产品经理·原型模式·web app