手势进阶:磁吸、跟随、滑动消除
拖拽惯性是把松手速度交给弹簧顺势抛出;本页是它的另一面——把持续的指针位置接到动画值上。
三个手势共用同一套零件:pointerdown / move / up 取输入、createSpring 平滑逼近目标、createDecay 接住松手速度。区别只在于「目标是什么、谁来接管」。
1 · 磁吸
指针进入按钮一定半径内,按钮被弹簧吸向指针:目标偏移等于指针到中心的向量乘一个比例系数。指针离开半径,目标偏移归零,弹簧把它弹回原位。
关键是弹簧的 from 始终取实时位置、to 取当下目标。目标随指针连续变化时,每一次重建都从当前值与当前速度续上,所以目标突变(比如指针跨过半径边界)也不会让按钮瞬跳——弹簧自然把这段差值演化掉。
2 · 跟随
小圆点用弹簧平滑跟随指针而非瞬移:每帧把弹簧目标更新为最新指针位置,值朝它逼近。欠阻尼参数下会带轻微延迟与过冲——指针骤停时圆点冲过一点再收回。
注 · 本页每次 pointermove 都重建一次弹簧,而 pointermove 在高刷屏上可以每秒触发上百次——比 rAF 还密。之所以不做节流,是因为重建的成本只是算几个系数(见向量弹簧的同一笔账),而节流会让目标滞后于指针,把「弹簧的滞后」与「采样的滞后」混在一起,调参时分不清哪个在起作用。
3 · 滑动消除
水平拖动卡片,松手时用最近两帧的位移除以时间估出速度。若位移或速度超过阈值,把这个速度交给 createDecay,卡片顺着惯性甩出并淡出;否则用 createSpring 弹回原位。
两条路的分叉点就是松手那一刻的一次比较。判定「消除」之后就是无目标的惯性滑行——惯性衰减那一套,落点由初速度决定。
警示 · 光靠 decay 的自然落点不够。慢速但超过位移阈值地松手时,
算出的落点可能还在屏幕内,卡片会停在半路。所以本页补了一道:若自然落点没冲出「舞台一半宽加一个卡片宽」,就用 modifyTarget 强制改到那个距离。物理手感与「必须消失」这个业务约束在此处冲突,解法与转盘落点的吸附是同一个——用
modifyTarget 在物理之外加一层约束。
import { createSpring, createDecay } from '@vega/anim';
// 磁吸 / 跟随: 每次目标变化都用「当前位置 + 当前速度」重建弹簧
function aim(tx, ty) {
const vx = sx.velocity(now - t0); // 接住进行中的速度
sx = createSpring({ stiffness, damping, from: cur.x, to: tx, velocity: vx });
t0 = now;
}
// 滑动消除: 松手时按阈值分叉
function onPointerUp() {
const v = (dx - lastX) / dt * 1000; // 最近两帧估速度 (px/s)
const over = Math.abs(dx) > THRESH_DX || Math.abs(v) > THRESH_V;
if (over) {
anim = createDecay({ velocity: v, from: dx }); // 无目标, 顺势甩出
} else {
anim = createSpring({ from: dx, to: 0, velocity: v }); // 弹回原位
}
}
松手抛出的惯性来源见拖拽惯性;弹簧的过冲与回弹形态见弹簧;不断改目标、由弹簧从当前位置与速度接管下一段,正是中断与重定向讲的速度接管。