今天给 Argos 做了一个“段位晋级彩纸特效”。
听起来是个很小的 UI 动画:用户 token 用量触发段位变化时,全屏弹一下彩纸,提示“黄金 III”之类的晋级结果。但真做起来,我发现这类效果很容易翻车。粒子太少,不像庆祝;粒子太多,像噪音。彩带想做飘逸,结果像屏幕上画了几条乱线。加爆点,本意是增强“爆炸感”,实际像廉价烟花。加光晕,本意是高级,实际会脏。
这篇文章复盘这次踩坑。重点不是 SwiftUI 代码,而是把粒子系统里那些概念讲清楚:particle、emitter、confetti、streamer、glint、ambient sparkles、bloom、burst、motion field、lifetime、distribution。以后再做类似 UI 特效,至少知道自己在调什么。
一句话总结
UI 里的庆祝特效,本质是一个小型 particle system。好看的关键不是“粒子越多越好”,而是让粒子的出生位置、生命周期、运动方向、密度分布、视觉层级服务同一个动势。
这次到底做了什么
Argos 里有一个段位系统:用户每天使用 Claude Code / Codex 的 token 会累计,达到某个 milestone 时触发段位变化。
我想要的效果是:
- 段位变化时出现全屏庆祝反馈。
- 中间有一个段位 toast,例如“黄金 III”。
- 屏幕上有彩纸、亮片、星芒,形成“晋级”的瞬间感。
- 只在调试环境有“测试彩纸”按钮,正式逻辑仍由真实段位变化触发。
最终通过的版本是:
| 元素 | 最终保留 | 原因 |
|---|---|---|
| Confetti rectangles | 是 | 语义最清晰,一眼就是庆祝 |
| Circular chips | 是 | 补充面积和层次,不抢主视觉 |
| Diamond glints | 是 | 增加高光和精致感 |
| Small cross sparkles | 是 | 少量点缀,强化“闪一下” |
| Streamers / 彩带 | 否 | 方向、曲线、交叉都很难调,容易显脏 |
| Burst / 爆点 | 否 | 在这个场景里太像烟花,不符合 macOS 工具气质 |
| Heavy bloom / 大光晕 | 否 | 容易污染画面,让文字和桌面背景变糊 |
这其实是一个很典型的 UI 粒子系统取舍:不是把所有效果都加上,而是删到只剩能稳定表达语义的元素。
Particle:粒子不是“点”,而是一个小演员
很多人听到 particle 会下意识理解成“点”。但在 UI 动画里,一个 particle 更像一个小演员,它至少有这些属性:
| 属性 | 含义 | 彩纸特效里的例子 |
|---|---|---|
| position | 当前坐标 | 彩纸当前在屏幕哪里 |
| velocity | 速度 | 往左上、右上、下落还是漂移 |
| acceleration | 加速度 | 重力、阻尼、回弹 |
| lifetime | 生命周期 | 0.8s 出现,3.5s 消失 |
| size | 尺寸 | 小彩纸、大亮片 |
| rotation | 旋转 | 彩纸翻转角度 |
| opacity | 透明度 | 出现、停留、淡出 |
| color | 颜色 | 高饱和彩纸色、湖水青、晨光金 |
| shape | 形状 | 矩形、圆形、菱形、星芒 |
所以“加一点彩纸”不是加几个随机矩形,而是定义一组小对象:它们什么时候出生、从哪里出生、往哪里走、多久消失、怎么旋转、颜色如何分布。
这次一开始怪异,根因就是我把粒子看成“画面装饰”,而不是一个系统。粒子的运动方向、密度和语义没对齐,就会出现“从上往下落”“一坨挤在一起”“不够满”“没有爆炸感”这些问题。
Emitter:发射器决定第一眼的动势
Emitter 是粒子系统里的发射器。它决定粒子从哪里出生。
常见 emitter 有几种:
| Emitter 类型 | 视觉效果 | 适用场景 |
|---|---|---|
| Point emitter | 从一个点喷出 | 烟花、爆炸、魔法点 |
| Line emitter | 从一条线喷出 | 雨、雪、瀑布、顶部落彩纸 |
| Area emitter | 从一个区域出现 | 环境漂浮、空间尘埃 |
| Edge emitter | 从屏幕边缘进入 | 庆祝彩纸、舞台入场 |
| Multi-emitter | 多个发射源 | 全屏庆祝、游戏技能 |
这次最大的教训是:发射器位置会强烈决定用户对动画的理解。
最开始用户反馈“怎么是从上往下落的”,这说明粒子的 emitter 或 velocity 让人联想到“掉落”,而不是“庆祝喷发”。彩纸如果主要从顶部进入,语义就偏“下雨”;如果从左右两侧或底部斜向上出现,语义才接近“庆祝”。
后来我尝试让彩带从左右两边飞出,但又出现了新问题:左右彩带在中间交叉、起点过于集中、曲线不丝滑。这说明 emitter 不是只看一个点,还要看发射区域分布。
更准确地说,emitter 要调这几个参数:
| 参数 | 作用 |
|---|---|
| origin | 发射源位置 |
| spread | 发射范围 |
| angle | 初速度方向 |
| variance | 随机扰动 |
| rate | 单位时间生成多少粒子 |
| burst count | 瞬间生成多少粒子 |
最终我放弃彩带后,保留了更稳定的 area/edge 混合:彩纸从不同区域出现,星芒覆盖更大的屏幕区域,但不再让连续线条承担主视觉。
Confetti:彩纸是最稳的庆祝语义
Confetti 就是彩纸。它是庆祝动画里最稳定、最不需要解释的元素。
UI 里常见的 confetti 粒子通常是:
| 形状 | 视觉特点 |
|---|---|
| 矩形条 | 最像真实彩纸 |
| 小方块 | 更像游戏奖励 |
| 圆片 | 像亮片或纸屑 |
| 菱形 | 更有闪光感 |
| 短弧片 | 更像翻转中的纸片 |
这次最终保留的是亮色矩形彩纸。原因很简单:它的语义最强。
彩纸要好看,核心不是形状复杂,而是三件事:
- 颜色要亮:庆祝粒子不能太灰,否则像灰尘。
- 尺寸要有层次:全是同一尺寸会机械。
- 旋转要变化:不旋转就像静态贴纸。
我一开始颜色不够亮,用户直接指出“彩纸色彩要亮”。这很关键。UI 主体可以克制,但庆祝粒子本身必须高饱和,否则它的情绪表达不成立。
这里有一个设计边界:背景和主 UI 需要低饱和,粒子可以高饱和。如果整个界面都高饱和,会廉价;但庆祝粒子高饱和,是语义需要。
Streamer:彩带为什么最容易翻车
Streamer 是长彩带、飘带。它的问题比彩纸复杂很多。
彩纸是离散粒子,单个粒子不好看也不会毁掉整体。彩带是连续曲线,一条不自然就非常显眼。
这次彩带翻车主要有几个原因:
| 问题 | 根因 |
|---|---|
| 太高 | lift 参数过大,曲线飞到屏幕顶部 |
| 太长 | reach 过大,线条穿过视觉中心 |
| 挤在一起 | emitter 起点过于集中 |
| 全是并行 | 控制点变化不足,曲线缺少角度差 |
| 交叉很尬 | 左右两侧没有限定各自活动区域 |
| 不丝滑 | Bézier 控制点没有形成自然切线 |
彩带要自然,至少要同时满足:
- 起点分布不能重合。
- 曲线控制点要有连续的切线方向。
- 同侧可以有交叉,但交叉角度要自然。
- 左右两侧如果语义是“各自飞出”,就不能互相穿越中心。
- 长度、弧度、透明度要一起衰减。
这就不是“加几条曲线”了,而是在调一个小型 motion field。
最后我判断:在这个产品场景里,彩带收益不够高。Argos 是一个 macOS 工具,不是游戏结算页。彩带一旦做不好,负收益很明显。所以最终删掉。
这个决策很重要:粒子系统不是越复杂越高级,删掉不稳定元素也是工程能力。
Glint:星芒负责“精致”,不要负责“热闹”
Glint 是闪光、星芒。它通常是菱形、四角星、十字星,出现在高光点附近。
它适合做:
- 金属反光。
- 奖励高光。
- 魔法感。
- “刚刚发生了好事”的瞬间提示。
但 glint 不能太多。太多会变成噪点,也会像廉价贴纸。
这次最终保留了两种 glint:
| 类型 | 作用 |
|---|---|
| Diamond glints | 菱形闪光,提供精致感 |
| Cross sparkles | 十字闪光,提供瞬间感 |
它们不是主角,主角仍然是 confetti。glint 的职责是让画面从“只有纸片”变成“有奖励感”。
一个实用规则:glint 的 opacity 应该随 pulse 变化,而不是常亮。常亮的星芒像贴图;一闪一灭才像反光。
Ambient Sparkles:环境闪光不是爆点
Ambient sparkles 可以翻译成环境闪光、氛围闪点。它和 burst 不一样。
| 概念 | 特点 | 观感 |
|---|---|---|
| Burst | 从中心或局部突然爆开 | 强刺激、像烟花 |
| Ambient sparkles | 分散在画面中轻微闪烁 | 氛围、精致、空间感 |
这次我试过加爆点,但用户反馈“不好看”。原因是爆点会把语义带向“烟花爆炸”,而 Argos 的晋级反馈更像“系统奖励”,不应该过度娱乐化。
Ambient sparkles 更适合这个场景:它们不抢段位 toast,不制造视觉中心,只是在背景层增加一点“庆祝正在发生”的环境反馈。
实现时要注意:
- 不要集中在一个点。
- 不要全部同步闪。
- 不要持续太久。
- 不要比主彩纸更亮。
Ambient sparkles 的价值是“补空气感”,不是“制造高潮”。
Bloom / Glow:光晕是调味料,不是主菜
Bloom 是光晕、泛光。Glow 也是发光,但在 UI 语境里更泛一点。Bloom 通常指亮部向周围扩散,让画面有曝光感。
光晕很容易让效果看起来“高级”,也很容易让画面变脏。
这次我对光晕的判断是:
| 光效 | 适合程度 | 原因 |
|---|---|---|
| 小面积 highlight | 高 | 能增强奖励感 |
| 粒子边缘轻微 glow | 中 | 可以让粒子更亮 |
| 大面积 bloom | 低 | 会污染桌面背景和文字 |
| 中央强光爆点 | 低 | 会抢段位 toast |
尤其在 macOS 上,全屏 overlay 下面是真实桌面和窗口。如果光晕太重,用户看到的是“脏”和“糊”,不是高级。
所以最后只保留轻量星芒和亮片描边,没有做大面积 bloom。
我的经验是:工具型产品里的 bloom,要像盐,不能像汤底。
Burst:爆点为什么不一定有“爆炸感”
Burst 是瞬间爆发。很多庆祝动画都会从一个中心点喷出大量粒子,形成爆炸感。
但 burst 有一个问题:它非常依赖上下文。
在游戏里,burst 很自然,因为奖励、击杀、升级本来就是强反馈。在 macOS menu bar 工具里,burst 太强就会显得浮夸。用户这次明确说“彩纸特效不要加爆点不好看”,这个判断是对的。
Burst 适合:
- 游戏结算。
- 支付成功。
- 大型 milestone。
- 首次完成关键任务。
Burst 不适合:
- 高频状态变化。
- 工具型产品里的轻量反馈。
- 已经有 toast 的场景。
- 背景复杂、可读性重要的场景。
段位晋级当然是好事,但它不是游戏胜利。Argos 的整体调性是克制的 macOS 工具,所以 burst 被删掉是合理的。
Motion Field:自然感来自“方向场”,不是 random
很多粒子系统不好看,是因为用了太多随机数。
随机数只能避免重复,不能自动产生自然感。自然感来自 motion field:一套整体一致的运动方向场。
这次我踩过的坑是:
- 每条彩带都有随机控制点,但整体没有一致方向。
- 左右彩带有交叉,但交叉没有空间逻辑。
- 起点随机,但集中在相近区域,实际观感还是一坨。
也就是说,random 只是 variation,不是 composition。
一个更靠谱的思路是:
1 | 先定义整体动势:从两侧 / 中下区域进入,向中上区域扩散,再缓慢下落淡出 |
如果反过来,先给每个粒子随机,再希望整体自然,通常会失败。
Distribution:满不满,取决于分布,不只取决于数量
用户后来反馈“感觉不够满”。第一反应容易是加数量。但只加数量会导致两个问题:
- 原来空的地方还是空。
- 原来挤的地方更挤。
所以要区分两个概念:
| 概念 | 解决什么 |
|---|---|
| Count | 粒子数量 |
| Distribution | 粒子分布 |
这次最终我同时做了两件事:
- 提高粒子数量:彩纸、亮片、星芒都增加。
- 扩大覆盖区域:横向更接近全屏,纵向从中下区域扩到中上/中下区域。
这才让画面变“满”。如果只加 count,不改 distribution,只会更乱。
一个实用判断:如果你觉得画面“不够满”,先看热力图,不要先加粒子。
Lifetime:动画太快,本质是生命周期太短
用户一开始还反馈“太快了”。这通常不是单个粒子速度的问题,而是 lifetime 的问题。
Lifetime 包含:
- 出生 delay。
- 可见时间。
- fade out 时间。
- 整体 overlay 持续时间。
如果所有粒子几乎同时出现、同时消失,即使速度不快,也会显得“啪一下没了”。如果 delay 分层,粒子批次进入,动画会更有呼吸。
这次我把整体展示时间拉长,并让不同粒子有不同 delay。这样用户看到的是一段庆祝过程,而不是一帧贴图。
Toast:中心信息不能和粒子抢主视觉
这个特效里还有一个中心段位 toast。
Toast 的任务是告诉用户:你升到了哪个段位。粒子的任务是表达情绪。二者不能互相抢。
我中间也犯过一个错误:段位 badge 设计太突兀,颜色和粒子不搭,又不够明显。后来调整为更克制的青山湖风格,用深绿/湖水青承载,标题变大,减少花哨颜色。
这里的原则是:
- Toast 负责信息。
- 粒子负责情绪。
- Toast 颜色要稳定,粒子颜色可以跳。
- Toast 不要跟粒子一样花。
如果中心信息也花,背景粒子也花,用户就不知道该看哪里。
Canvas 实现:为什么不用一堆 SwiftUI View
这次最终用 SwiftUI Canvas 画粒子,而不是 ForEach 渲染一堆 View。
原因很直接:
| 方案 | 优点 | 问题 |
|---|---|---|
| 多个 SwiftUI View | 写起来直观 | 大量 View diff,动画状态复杂 |
| Canvas | 单层绘制,适合粒子 | 要自己管理坐标、时间、路径 |
粒子系统本质上是每帧根据时间计算位置,然后画出来。Canvas 更贴近这个模型。
大致结构是:
1 | TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { timeline in |
elapsed 是整个系统的时钟。每个粒子根据 elapsed - delay 算自己的局部时间,再算 position、rotation、opacity。
这个模型比给每个粒子挂一堆 .animation 更可控。
这次最后留下的系统结构
最终版本可以理解成 3 层:
| 层级 | 元素 | 作用 |
|---|---|---|
| 主粒子层 | Confetti rectangles | 庆祝语义 |
| 辅助粒子层 | Circular chips | 增加密度和面积 |
| 高光层 | Diamond glints + cross sparkles | 精致感、瞬间感 |
被删掉的是:
| 元素 | 删除原因 |
|---|---|
| Streamers | 曲线难自然,抢画面 |
| Burst | 太像烟花,不符合产品气质 |
| Heavy bloom | 污染背景,可读性差 |
这个结构其实很朴素,但稳定。
很多 UI 特效最后不是赢在复杂,而是赢在“每个元素都知道自己负责什么”。
我的判断框架
以后再做类似粒子特效,我会按这个顺序判断:
| 问题 | 优先调什么 |
|---|---|
| 不像庆祝 | emitter 方向、颜色亮度、confetti 形状 |
| 太乱 | 删除连续元素,降低 bloom,减少同步运动 |
| 不够满 | distribution 先于 count |
| 太快 | lifetime、delay、fade out |
| 不自然 | motion field 先于 random |
| 主信息不明显 | toast 层级、粒子避让、颜色对比 |
| 廉价 | 少用爆点和大光晕,控制高饱和面积 |
一句话:先定语义,再定动势,最后才调随机参数。
要不要用粒子系统
我的判断是:值得用,但要克制。
适合用的场景:
- 用户完成一个阶段性目标。
- 系统需要表达奖励。
- 状态变化低频且重要。
- 动画不会打断用户工作流。
不适合用的场景:
- 高频反馈。
- 错误提示。
- 信息密集页面。
- 用户正在专注输入或阅读。
Argos 这次段位晋级适合用粒子系统,因为它是低频、正向、阶段性的反馈。但最终也证明了一点:工具型产品里的庆祝动画,不应该朝游戏化一路狂奔。真正合适的方向是:有情绪,但不喧宾夺主。
这也是我对这次迭代最大的收获。



