进度条:把跳变的真实进度译成可信的运动
进度条看似只是「一条会变长的方块」,真正的难点全在动画这一侧:真实进度往往是从外部
离散跳变着到的 (下载分块 0 → 23 → 23 → 71 → 100),直接把宽度设过去会一段段跳变;
有时压根测不到真实进度 (跨页跳转、等待接口),只能给出「正在进行」的体感;有时它是
多段的 (stories),需要可暂停、可跳段的播放控制。把这些都收敛成一句话:进度条是把一个
不连续、甚至不可知的外部信号,翻译成视觉上连续而可信的运动。
本页用 @vega/anim 把五种形态走通,全程不依赖 CSS transition /
animation:确定态用弹簧值源做平滑追赶、不确定态用
repeat:Infinity 循环并在拿到总量后落定 (与转盘落点同构)、
trickle 用渐近曲线诚实地骗、分段用一条时间轴游标支撑暂停 / 跳段、
圆环则把同一个 0→1 同时喂给 conic-gradient 与 SVG stroke-dashoffset。
确定态:让显示值平滑追赶真实进度
最常见的形态:真实进度已知,但它是分块跳着到的。把宽度直接设成真实值,每块到达都会「啪」地
跳一下;正确做法是真实值只当目标,显示值由一个弹簧持续追它。每块到达时
animateTo(目标) 以当前显示值 + 当前速度为新初始条件重建弹簧——速度连续,不会从静止
重启而顿挫。下图蓝线是真实进度 (阶梯),橙线是显示进度 (被弹簧抹平的追赶轨迹)。
value / velocity 续上的,这正是 animateTo 相对「cancel 旧动画再
起一条」的根本优势 (见飞行中重定向)。
不确定态:先空转,拿到总量再落定
当总量未知 (服务端还在统计、流式响应没有 Content-Length),进度条无法给出百分比,只能表达
「正在进行」:一段repeat:Infinity 的循环——滑块来回扫,或条纹斜向流动。一旦拿到
总量,就交接成确定态:停掉循环,从 0 按 easeOut 填到 100%。这与转盘落点
的「不定 → 定」是同一个结构——空转是占位,落定才是结果。
测不到真实进度:trickle 的「诚实假进度」
单页应用路由切换、首屏白屏期——这些场景没有可读的进度,但用户需要反馈。业界做法 (NProgress 一类)
是给一条假进度:朝一条渐近线 (如 90%) 减速逼近、永不到顶,真正完成时再一把
snap 到 100%。关键在那条永不触及的上限——它诚实地承认「我不知道还剩多少」,只保证
「还在动」。这里用一段 expoOut 把 0 推向上限:前期快、越接近越慢,天然的渐近形状。
expoOut 的速度
越趋于零。
分段进度:stories 与时间轴游标
Instagram / WhatsApp 的故事条是 N 段独立进度的串联:每段定时填满后自动推进到下一段。它本质是一条
时间轴:整体进度 p ∈ [0, N),第 ⌊p⌋ 段正在填,段内
进度 p − ⌊p⌋。于是暂停 / 继续就是播放器的 pause / play,
跳段就是把游标 seek 到段边界——播放控制天然支撑这些交互。
seek 到段边界、不打断播放;播放 / 暂停切的是同一个
游标的运行状态。因为「时间 → 各段填充」是纯函数,跳到哪一段都能立刻画对,这就是把 stories 建在
播放器之上的好处。
圆环进度:同一个 0→1,两种画法
把进度画成圆环有两条互不相干的实现路径,却吃同一个 0→1:其一 conic-gradient——按角度
扫出扇形,叠一个内圆挖空成环;其二 SVG stroke-dashoffset——把虚线段长设成
周长,dashoffset = 周长 × (1 − p) 推出实线弧。拖滑块看两者严丝合缝地同步;开不确定态
则是一段固定弧 + repeat:Infinity 的旋转,即最常见的 spinner。