进度条:把跳变的真实进度译成可信的运动
进度条看似只是一条会变长的方块,难点全在动画这一侧:真实进度往往从外部离散跳变着到(下载分块 ),直接把宽度设过去会一段段跳;有时压根测不到真实进度;有时它是多段的,需要可暂停、可跳段的播放控制。
收敛成一句话:进度条是把一个不连续、甚至不可知的外部信号,翻译成视觉上连续而可信的运动。本页五种形态全程不依赖 CSS transition 与 animation。
1 · 确定态的平滑追赶
真实进度已知,但它是分块跳着到的。把宽度直接设成真实值,每块到达都会「啪」地跳一下;正确做法是真实值只当目标,显示值由一个弹簧持续追它。每块到达时 animateTo 以当前显示值与当前速度为新初始条件重建弹簧。
无论参数怎么变,连点「手动来一块」都不会看到跳变——因为新目标是从当前 value 与 velocity 续上的,这正是 animateTo 相对「取消旧动画再起一条」的根本优势。
2 · 不确定态与落定
总量未知时(服务端还在统计、流式响应没有 Content-Length),进度条无法给出百分比,只能表达「正在进行」:一段 repeat: Infinity 的循环。一旦拿到总量就交接成确定态。
这与转盘落点是同一个结构——空转是占位,落定才是结果。区别在于转盘的交接要保住速度连续,而进度条的落定重新从 0 填起——循环占位本就没有「当前进度」这个量。
3 · trickle 的诚实假进度
单页应用路由切换、首屏白屏期这些场景没有可读的进度,但用户需要反馈。业界做法是给一条假进度:朝一条渐近线减速逼近、永不到顶,真正完成时再一把 snap 到 100%。
注 · 上限那条红线是 trickle 的灵魂,它故意到不了。若让假进度跑到 100% 却迟迟不结束,反而比卡在 90% 更伤信任。本页用 expoOut 把 0 推向上限:前期快、越接近越慢,天然的渐近形状——expoOut 的值是
,理论上永远差一点点,正好符合「不知道还剩多少」的语义。
4 · 分段进度是一条时间轴
故事条是
段独立进度的串联。它本质是一条时间轴:整体进度
,第
段正在填,段内进度
。于是暂停与继续就是播放器的 pause 与 play,跳段就是把游标 seek 到段边界。
「上一段」有个容易忽略的细节:段内进度不足 8% 时跳到上上段边界,否则只跳回本段开头——这是媒体播放器普遍的行为,刚播两秒按上一首是回到本首开头,播到一半按才是真的上一首。
5 · 圆环的两条实现路径
把进度画成圆环有两条互不相干的实现,却吃同一个
:conic-gradient 按角度扫出扇形再叠内圆挖空;SVG 的 stroke-dashoffset 把虚线段长设成周长,
推出实线弧。
警示 · 两条路径的起始角不同。conic-gradient 从 12 点方向顺时针扫,而 SVG 圆的 0° 在 3 点方向,所以 .rarc 必须额外 rotate(-90deg) 才对得上。开 spinner 时这个
要叠进旋转量里(rotate(deg − 90)),漏掉就会看到两个环差四分之一圈。
const disp = new Animation({
spring: { from: 0, to: 0, stiffness, damping },
onUpdate: v => bar.style.width = clamp01(v) * 100 + '%',
});
function onChunk(realProgress) {
disp.animateTo(realProgress); // 以当前 value + velocity 续上, 速度连续
}
// 圆环: 同一个 p 喂给两条路径
const C = 2 * Math.PI * r;
arc.style.strokeDasharray = C;
function setProgress(p) {
const deg = p * 360;
conic.style.background =
`conic-gradient(var(--accent) ${deg}deg, var(--accent-soft) ${deg}deg)`;
arc.style.strokeDashoffset = C * (1 - p);
}
五种形态拆开是五个交互,合起来是同一条主线:能测就平滑追赶、不能测就诚实渐近、多段就交给游标。值怎么算、时间怎么塑形、播放怎么控制,正是本系列从插值到播放控制的同一套地基落到 UI 上。