lerp 跟手:别把拖拽元素硬吸附在光标上
拖拽交互里最容易忽略的一处手感:每一帧把被拖元素的位置直接赋成光标位置(snap,直接吸附),元素与手指严丝合缝、绝对精确,但运动很「硬」,没有任何缓冲与重量感。游戏开发界的常用做法是改用 lerp(linear interpolation,线性插值):元素每帧只朝光标追近一段,而不是一步到位——于是产生一点点滞后追赶的惯性,整体顺滑得多。
关键认识:lerp 的系数 t 哪怕设得很高(很「跟手」),也仍比直接吸附柔和。下面的演示让 snap 与 lerp 两块同时追同一个光标:慢慢拖时两者几乎重合,一旦快速甩动,紫色的 lerp 块就会拖出一道平滑的尾迹去追赶,而灰色的 snap 块永远死死贴着光标。本页是纯 JS(requestAnimationFrame)驱动的,与本系列其余纯 CSS 的声明式做法互补——连续跟随光标 / 手势这类交互,缓动通常落在脚本而非声明式动画里。
1 · 同一个光标,snap vs lerp 同台
在方框内按住拖动。灰块 snap 永远等于光标位置;紫块 lerp 用你设定的系数 t 追赶。打开尾迹能看清两者路径的差别:snap 的尾迹就是手的原始轨迹(带折角),lerp 的尾迹被抹平成圆滑曲线。
把 t 拖到接近 1,lerp 块几乎瞬间贴上光标、与 snap 重合——这就是「直接吸附」。往小拖,滞后追赶越明显、惯性越重。妙处在于:即便取 0.6、0.8 这种很高的值(快得几乎跟手),甩动时仍能看出一道柔和的缓冲,而非 snap
那种生硬的瞬移。一点点延迟,换来明显更舒服的手感。
2 · 两种写法只差一行
拖拽逻辑不变——pointermove 只负责更新目标(光标位置),不直接移动元素;真正决定手感的是每帧 rAF 里那一行:是把当前位置赋成目标,还是朝目标插值。
pointermove 只更新目标,不动元素:
let tx = 0, ty = 0;
let px = 0, py = 0;
const t = 0.18;
stage.addEventListener('pointermove', (e) => {
const r = stage.getBoundingClientRect();
tx = e.clientX - r.left;
ty = e.clientY - r.top;
});
snap 直接吸附(硬)——每帧把当前位置赋成目标:
function frame() {
px = tx;
py = ty;
el.style.transform = `translate(${px}px, ${py}px)`;
requestAnimationFrame(frame);
}
lerp 线性插值(软)——每帧只朝目标追近一段:
function frame() {
px += (tx - px) * t;
py += (ty - py) * t;
el.style.transform = `translate(${px}px, ${py}px)`;
requestAnimationFrame(frame);
}
3 · 掉帧也别改手感:按 dt 校正
上面的 t 是「每一帧」追近的比例,因此手感隐含绑定在帧率上:同样的 t,120Hz 屏比 60Hz 追得快一倍,掉帧时又会变慢。游戏里的修正做法是按两帧间隔 dt 把系数校正一次,让「每秒追近多少」恒定,与刷新率无关。本页演示用的就是这个版本。
let last = performance.now();
function frame(now) {
const dt = (now - last) / 1000;
last = now;
const a = 1 - Math.pow(1 - t, dt * 60);
px += (tx - px) * a;
py += (ty - py) * a;
el.style.transform = `translate(${px}px, ${py}px)`;
requestAnimationFrame(frame);
}
什么时候不该 lerp。需要像素级精确落点的拖拽(对齐网格、裁剪手柄、绘图工具的控制点)应当 snap——滞后会让人对不准。lerp 适合「拖卡片 / 拖列表项 / 跟随光标的指示物」这类追求手感、不要求逐帧精确的场景。另外要尊重
prefers-reduced-motion:命中时把 t 调到 1(等价 snap)或直接关掉缓冲;并在 pointerup 后让元素 lerp 到最终落点收尾,而不是戛然而止。手感终究以实际拖动的观感为准。
4 · 相关链接
- Lerp smoothing is broken · Freya Holmér——逐帧 lerp 的帧率依赖问题与 dt 校正(指数平滑)的来龙去脉。
- Game Loop · Game Programming Patterns——固定步长 / 可变步长游戏循环,dt 校正思路的出处。
- Pointer events · MDN
- requestAnimationFrame() · MDN
- 好动画的七条准则 · 自然 · 本系列
- 性能与取舍:该用 CSS 还是 JS? · 本系列