动力 · 18
惯性衰减
Inertia · inertia从释放速度按时间衰减,并在释放瞬间预知落点。
拖动的物体松手后不该立刻停下。速度指数衰减可以精确积分,落点是当前位置加速度乘时间常数。若每帧都简单乘同一个摩擦系数,刷新率一变,走过的距离也会变。
公式
v(t) = v₀·e^(−t/τ) x(t) = x₀ + v₀·τ·(1 − e^(−t/τ)) landing = x₀ + v₀·τ
参数
| 本页默认 | 成熟实现 | |
|---|---|---|
| timeConstant | 0.35 s | Motion 325 ms |
| power | velocity × τ | Motion power 0.8 |
怎样做好
- 在松手那一刻先计算最终落点,可用它选吸附目标。motion packages/motion-dom/src/animation/generators/inertia.ts:35–39
- 速度与位移都按 dt 的指数解推进;固定每帧摩擦在高刷新率下会改变衰减和距离。
- Motion 惯性生成器默认 power 0.8、timeConstant 325 ms,并通过
modifyTarget调整目标。motion packages/motion-dom/src/animation/generators/inertia.ts:7–16,35–39
在我们的案例里
19 个案例使用惯性。momentum-slider、drag-gallery 和 formation 都把拖动转换为持续运动;phase-2 对这类逐帧积分另有质量标记,提醒检查刷新率依赖。
49 / 69 个案例 · 有逐帧积分(未乘 dt)来自 Codex 逐案质量标记(按关键词归类,近似值)
出现在 19 个案例
参考实现 · basics.js
/**
* Exponential decay after a throw. v(t) = v0·e^(−t/τ), so it lands at x0 + v0·τ.
* τ ≈ 0.5 s matches iOS UIScrollView "normal" (decelerationRate 0.998 per ms); 0.1 s ≈ "fast".
*/
export class Inertia {
constructor({ value = 0, velocity = 0, timeConstant = 0.35, min = -Infinity, max = Infinity } = {}) {
Object.assign(this, { value, velocity, timeConstant, min, max });
}
/** Where it will come to rest if nothing intervenes. */
get projected() { return this.value + this.velocity * this.timeConstant; }
step(dt) {
const decay = Math.exp(-dt / this.timeConstant);
this.value += this.velocity * this.timeConstant * (1 - decay); // exact integral over dt
this.velocity *= decay;
if (this.value < this.min || this.value > this.max) {
this.value = clamp(this.value, this.min, this.max); this.velocity = 0;
}
if (Math.abs(this.velocity) < 0.5) this.velocity = 0;
return this.value;
}
}
/** The per-frame version found in our cases: `x += v; v *= friction` — speed depends on refresh rate. */
export const inertiaPerFrame = (state, friction) => { state.value += state.velocity; state.velocity *= friction; };