转盘落点:目标未知时,先空转再落定
抽奖转盘、老虎机卷轴、扭蛋这类交互有一个共同的结构性难点:点击的那一刻落点还不知道—— 它要等接口返回。于是动画被拆成两段不同性质的运动:先是一段不定时长的匀速空转 (等待网络), 数据回来后再接一段定时长的减速,精确落到中奖扇区。业界称这种形态为 不定 → 定 (indeterminate → determinate):转的过程是占位,落定才是结果。
本页用 @vega/anim 把这条线走通,不依赖 CSS animation。两段运动分别是两个
Animation:匀速段是 ease:'linear' 的无限循环,落定段在接口返回后才按当前角度
新建。衔接的关键在于落定段的起点必须等于循环段被打断那一刻的真实角度,否则会跳一下。
右侧画出整段运动的角速度剖面 (deg/ms)。空转期是一条水平的平台 (匀速,deg/ms 恒定);
虚线标出接口返回、运动接管的交接点;之后曲线随落定缓动一路衰减到 0。这张图正是
「不定 → 定」最直观的样子:平台的长度不可预知,衰减段的形状由 落定曲线 决定。
默认的物理减速把落定交给惯性模型 decay:不预设目标曲线,只给一个初速度
与摩擦,任其滑行到停 (与动量滚动、惯性列表同源)。它和 缓动里的固定曲线在因果上正相反
——固定曲线先定时长、再塑形;惯性减速反过来,时长与滑行距离都由物理反推。唯一的约束是落点必须命中扇区,
于是用 modifyTarget 把理论落点吸附到最近的中奖角度,既保留物理手感、又保证结果精确。
decay 是基于物理的减速:它把交接那一刻的真实角速度
当作初速度,之后只有摩擦——角速度按 e^(−t/τ) 指数衰减,像松手的转盘自己滑行到停。
滑多远、停多久都不是设定的,而是 v₀ 与摩擦 τ 算出来的;时长由 settleTime = τ·ln(振幅/restDelta) 反推。
为让起速续上空转,这里把 power 取作 τ 秒 (振幅 = power·v₀ = τ·v₀,故吸附前的起点角速度
= 振幅/τ = v₀);落点再经 modifyTarget 吸附到最近的中奖扇区——这一步会让起速在 v₀ 上下略作增减,
但不会出现 easeOut 那种系统性的约两倍提速,所以右图平台大体平滑地接入一条指数衰减尾,
而非平台尾端那个突兀的向上尖峰;调大 τ 摩擦更小、滑得更久更远。再把落定曲线切到固定曲线对比:
easeOut 时长由滑块给定,起点角速度恒为该段平均的两倍,
而平均又约等于空转速度,所以交接处会突然提速一下——平台尾端那个向上的尖峰;match
用 cubicBezier(⅓, m₀/3, ⅔, 1) 按当前匀速反算起点斜率,尖峰消失,但圈数 / 时长仍是设定值;
backOut 会先冲过扇区再弹回,是转盘那种「咔哒一下到位」的手感来源。
匀速循环靠 repeat:Infinity + ease:'linear',见
播放控制的 repeat 语义;落定曲线的过冲来自 backOut,
见缓动里的 back 族;不用 弹簧收尾是因为弹簧的
过冲会先越过目标扇区再回摆,落点不可控,而 backOut 的过冲量固定、终值精确落在目标上。
默认的物理减速选 decay 而非弹簧亦是同理:decay 单调趋近落点、不过冲,
天然适合「必须停在扇区」——弹簧的目标吸引与回摆反而会把指针荡过中奖区。