最近我给个人主页上的 Zack 做了一轮人物动画。
一开始只是觉得,主页里已经有几个固定的人物场景,如果人物始终保持静态,会有一点像插图。既然 Zack 本身就是主页视觉的一部分,可以试着让几个场景各自动起来一点。
最后上线的是四组动作:
- Hero:挥手
- ON MY DESK:喝水
- STILL THINKING:思考和问号
- OFF THE SCREEN:和囡囡互动
一共 18 帧正式动画素材。

整个过程比我最开始想的要复杂一些。真正麻烦的地方,不是“让图片按顺序播放”,而是怎么把 AI 生成的东西整理成一套真正适合网页使用的素材。
先把动作做出来
这次没有重新设计 Zack 的形象,而是沿用主页里已经确定的人物风格,在现有静态角色基础上生成不同动作。
最开始尝试的动作并不都成功。
例如走路动画就做过一轮,但逐帧看以后问题很明显。连续几帧里腿部关系不自然,动作没有形成正常的步态循环,看起来更像几张姿势相近的图,而不是一个可以连续播放的 walking cycle。
最后这组没有上线。

生成过程中还遇到过其他问题。
有些帧里会突然多出原图没有的配饰;有时候人物动作本身可以接受,但细节在不同帧之间不稳定;也有生成结果直接把几格动作放在一张图里,这种图可以拿来观察动作趋势,但不能直接作为网页动画帧。
所以这一步并不是:
生成 18 张图,然后放进网站。
而是反复生成、筛选、淘汰,再把真正可用的帧留下来。
最后确定了四组动作:
Wave
4 帧。
Hero 里的 Zack 挥两轮手,然后停下来,不做无限循环。
我不希望主页上的人物一直重复挥手,那样注意力会被持续吸走。
Working
6 帧。
电脑前保留原本的工作状态,在这个基础上增加喝水动作和 idle timing。
这一幕的重点不是“展示动画”,而是让原来的静态场景稍微有一点生命感。
Thinking
4 帧。
保留思考动作,同时继续使用原来的问号动画。
With Nannan
4 帧。
最后一个场景里 Zack 和囡囡做一次简单互动,同样不是无限循环。
四组一共 18 帧。
动起来以后,还要重新看页面比例
桌面端完成以后,我又在手机上看了一遍。
这时发现另外一个问题:动画虽然正常,但 02 / ON MY DESK 和 03 / STILL THINKING 两幕里的 Zack 在手机屏幕上明显偏小。
桌面端看起来合理的比例,到了 390px 左右的页面宽度以后,人物存在感会下降很多。
最终移动端单独调整:
- ON MY DESK 约放大 50%
- STILL THINKING 约放大 40%
同时重新检查人物有没有遮挡正文、超出页面或者切掉关键部位。
这部分和动画本身没有关系,但也让我意识到,人物素材上线以后不能只检查“有没有动”。
动画是否正确、人物尺寸、页面留白和移动端视觉关系,本质上是同一个最终结果。
第一版上线以后,问题马上暴露了
视觉和动作确认以后,我把这套人物系统正式上线。
功能上没有问题:
- 挥手两轮后停止
- 喝水正常
- 思考和问号正常
- 囡囡互动正常
- 手机端尺寸也符合预期
但第一次打开网站,很明显感觉人物出来得太慢。
有时候进入页面以后要等一段时间,快速滚到后面的场景也会看到人物区域迟迟没有准备好。
我一开始也怀疑过托管和 CDN。
继续检查资源以后,很快发现问题主要在我自己。
18 张正式动画 PNG 合计 26.512 MiB。
分组大致是:
| 动作 | 原始 PNG |
|---|---|
| wave | 6.601 MiB |
| working | 8.826 MiB |
| thinking | 6.191 MiB |
| 囡囡互动 | 4.894 MiB |
| 合计 | 26.512 MiB |
这些原图的分辨率大约在 1086×1448 或 1448×1086。
作为 AI 生成的源文件没有什么问题。
但人物在网页里的实际显示尺寸远远没有这么大。
更麻烦的是第一版的加载方式。
当某个动作需要出现时,代码会一次创建这一组所有帧,等待所有图片完成下载和 decode,然后才把整组标记成 ready。
也就是:
动作进入当前场景
→ 下载整组图片
→ 等全部解码完成
→ 才显示动画
如果网络条件一般,人物自然就会迟到。
这时问题已经很清楚了。
不是动画逻辑错了,而是我把生成素材直接当成了网页生产素材。
重新处理 production asset
这一轮没有再改动作设计。
18 张原始 PNG 继续保留,作为 source asset 和后续修改时的源文件。
真正给主页使用的版本重新处理。
四组动作分别整理成四张支持透明通道的 WebP sprite:
- wave
- working
- thinking
- nannan
于是线上动画资源从:
18 张独立 PNG
变成:
4 张 production sprite

同时按照人物实际 CSS 显示尺寸重新控制分辨率,大约保留 2× 显示尺寸,足够覆盖高 DPI 屏幕,又不再把明显过剩的原始分辨率直接送到浏览器。
WebP 使用质量 92,透明通道质量 100。
处理后的体积:
| 动作 | 优化前 | 优化后 |
|---|---|---|
| wave | 6.601 MiB | 0.518 MiB |
| working | 8.826 MiB | 0.368 MiB |
| thinking | 6.191 MiB | 0.399 MiB |
| 囡囡互动 | 4.894 MiB | 0.648 MiB |
| 合计 | 26.512 MiB | 1.933 MiB |
总体积下降 92.71%。
逐帧检查以后,没有看到值得为了继续省体积而接受的明显画质损失,所以没有再继续追求更低的数字。
资源变小还不够
如果只是把 PNG 换成 WebP,问题只解决了一半。
我同时重新整理了加载逻辑。
现在主页打开以后,不会一次把四组动画全部下载下来。
大致顺序变成:
页面打开
→ 当前静态 Zack 正常显示
→ 优先准备当前场景动画
→ 动画 ready 后切换
→ 后台继续准备下一个场景
顺序基本是:
wave
→ working
→ thinking
→ with-nannan
还有一个比 preload 更重要的改动:
动画没有准备好时,静态人物不能消失。
第一版里存在一种情况:动画尚未 ready,但静态人物已经被隐藏,于是用户看到的就是一个空位置。
现在静态 Zack 始终作为 fallback。
无论动画正在加载、decode 失败、资源请求失败,甚至 JavaScript 没有正常运行,页面至少应该先保留静态人物。
我的目标变成了:
网络差的时候,可以晚一点动,但不能晚一点才有人。
这比单纯追求动画尽快 ready 更重要。
实际结果
为了确认这次修改不是单纯“感觉快了”,我做了一组冷缓存、4 Mbps 带宽、100 ms 延迟的受控测试。
优化前:
- Hero 静态人物出现:11.98 秒
- 挥手动画 ready:27.12 秒
优化后:
- Hero 静态人物出现:0.83 秒
- 挥手动画 ready:2.61 秒
这组数据当然不代表所有人的真实网络环境,但前后条件一致,已经足够看出差异。
上线前后也重新检查了:
- 桌面端
- 390px 手机端
- 四组完整动作
- 挥手停止逻辑
- 问号
- 快速上下滚动
prefers-reduced-motion- sprite 404
- decode failure
- 无 JavaScript fallback
手机端之前调整过的 02 / 03 人物尺寸也保持不变。
这轮到这里就结束了。
这次真正留下来的东西
如果只看最后的实现,这件事并不复杂:
PNG 换成 WebP,减少请求,按场景预加载,保留静态 fallback。
这些都是很常见的 Web 做法。
但这次对我更有用的结论是:
AI 生成素材只是 source asset,不等于 production asset。
一张 AI 图片在屏幕上“看起来已经完成”,并不意味着这个文件就适合直接交给网站。
至少还要继续判断:
- 尺寸是不是明显过剩
- 文件格式是否适合网页
- 多帧素材应该怎么组织
- 什么时候加载
- 加载失败以后显示什么
- 手机端是否仍然成立
- 动画本身是不是值得一直循环
以后如果再给 Zack 增加新动作,我应该不会再把生成出的原始 PNG 直接接进网页。
现在这套流程已经比较明确:
生成原始动作帧
→ 筛选和淘汰
→ 保留 source asset
→ 生成网页 production asset
→ 按场景渐进加载
→ 静态人物作为 fallback
→ 桌面和手机重新检查
这也是这轮主页人物动画真正完成以后,我觉得最值得留下来的部分。