转盘落点:目标未知时先空转再落定
抽奖转盘与老虎机是一类特殊的交互:点击那一刻目标还不存在。接口尚在路上,动画却必须立刻响应。做法是把整段运动拆成两段——先用 repeat: Infinity 加 ease: 'linear' 匀速空转占位,接口返回后再按当前角度新建一段减速,落到中奖扇区。
1 · 两段接力
空转段的长度不可预知(取决于网络),落定段却必须精确停在扇区中心。对齐靠一个取模:先算出目标角度与当前角度的差 ,再加上整数圈。
角速度剖面把这件事画得最清楚:空转期是一条水平的平台(匀速,deg/ms 恒定),虚线标出交接点,之后曲线随落定缓动衰减到 0。平台的长度不可预知,衰减段的形状由落定曲线决定。
2 · 交接处的速度
默认的落定曲线是惯性衰减而非固定曲线,因为两者在因果上正相反:固定曲线先定时长、再塑形;惯性减速反过来,时长与滑行距离都由物理反推。
要让起速续得上空转,本页把 power 取作
秒。振幅
,于是吸附前的起点角速度
,正好等于空转的角速度。惯性衰减那页记过 power 与
一般并不相等、起速会是标称值的若干倍——本页是那条关系被反过来用的一个实例。
警示 · 切到 easeOut 会在交接处看到平台尾端一个向上的尖峰。原因是 easeOut 的起点斜率恒为该段平均速度的两倍,而平均速度又约等于空转速度,于是交接瞬间凭空提速一倍。match 档用
按当前匀速反算起点斜率,尖峰随之消失——代价是圈数与时长仍是设定值,不再由物理决定。
3 · 为什么不用弹簧收尾
弹簧的目标吸引与回摆会把指针荡过中奖区再摆回来,落点在过程中不可控。decay 单调趋近落点、不过冲,天然适合「必须停在扇区」。
需要「咔哒一下到位」的手感时,backOut 是可控的折中:它也过冲,但过冲量固定、终值精确落在目标上——与弹簧的区别在于它是一条给定的曲线,越界的幅度写死在参数里。
const PER_REV = 720;
loop = new Animation({
keyframes: [0, 1], duration: PER_REV,
ease: 'linear', repeat: Infinity,
onUpdate: () => render(loop.currentTime / PER_REV * 360),
});
const win = await fetchResult();
const cur = loop.currentTime / PER_REV * 360;
const v0 = 360 / PER_REV * 1000;
loop.pause();
const base = cur + mod360(360 - sectorCenter(win) - cur);
const decay = createDecay({
from: cur, velocity: v0,
power: tau / 1000, timeConstant: tau,
modifyTarget: (ideal) =>
base + Math.max(0, Math.round((ideal - base) / 360)) * 360,
});
匀速循环的 repeat 语义见播放控制;backOut 的过冲来自 back 族缓动,见缓动;modifyTarget 与惯性吸附见惯性衰减。带速度接管的通用做法见中断与重定向。