动力 · 16
阻尼跟随
Damp · damp每一刻都把当前值拉近目标一部分。关键是按时间算,不是按帧算。
x += (target − x) * k 是站里最常见的平滑写法(53 个案例),但它默认每秒正好 60 帧:同样的 k 在 120 Hz 屏幕上跟得快得多,在掉帧时又变慢。把每帧的系数换成 1 − e^(−λ·dt),同样的时间就得到同样的位置,和刷新率无关。
公式
x ← x + (target − x) · (1 − e^(−λ·dt)) λ = −60 · ln(1 − k) port a 60 Hz factor k exactly half-life = ln 2 / λ time to close half the gap
参数
| 本页默认 | 成熟实现 | |
|---|---|---|
| k (60 Hz) | 0.1 | Lenis lerp: 0.1旧写法的每帧系数,只用来换算 λ。 |
| λ | −60·ln(0.9) ≈ 6.32 /s | Lenis 用 lerp × 60 = 6 /s60k 是近似值,k = 0.1 时偏慢约 5%;k 越大偏差越大。 |
| half-life | ≈ 110 ms | —用半衰期调参更直观:“半个差距要多久”。 |
| dt clamp | ≤ 40 ms | Motion maxElapsed = 40 |
怎样做好
- 乘时间,不乘帧数:
lerp(x, y, 1 − e^(−λ·dt))。这是 Lenis 的damp(),也是 Rory Driscoll 2016 年那篇“帧率无关阻尼”的结论。lenis packages/core/src/maths.ts · animate.ts:43 - 迁移旧常量要精确换算:
λ = −60·ln(1 − k)。直接用60k会让 k 大的场景(如 0.2、0.3)明显变慢。 - 钳住 dt。标签页切回来时第一帧可能是 10 秒,dt 不钳住会一步跳到终点;钳到 40 ms 让过渡继续可见。motion-dom/src/frameloop/batcher.ts:6
- damp 是一阶的:目标一跳,速度也立刻跳。需要“起步也柔和”或带惯性的跟随,用弹簧。
在我们的案例里
49 个案例里至少有一处逐帧积分(x += (t − x) * k、v *= k);Codex 读到的 325 次动力学使用里,119 次按帧推进,只有 18 次乘了 dt。rewrite/lib/case-runtime.js 的 lifecycle.animate 其实每帧都传入了钳制后的步长 step,案例只是没用上。
49 / 69 个案例 · 有逐帧积分(未乘 dt)来自 Codex 逐案质量标记(按关键词归类,近似值)
出现在 53 个案例
01Momentum Slider03Formation04Cardwall05Riffle06Revolver07Rotunda09Lens Bloom10Frieze11Aperture12Folio13Cinema Reel14Stream Showcase15Hardback16Picasso Ecommerce17Assembly18Fathom19Emulsion20Portfolio Photographer
+35 更多
21Reverie22Boutique23Nova25Shape Trail27Liquid Type28Filament29Pixels33Crema34Aster35Spring37Amour38Fluid Gallery39Marathon44Atlas45Aurora46Rooster Rush48Moments49Burst50Dimension51Morph52Meow53Relay54Lumen55Serene57Atrium58Skyward59Helix60Crate61Prism62Flashlight63Viewfinder64Sand66Tunnel67Reel69Loupe
参考实现 · basics.js
/**
* Exponential approach: frame-rate independent replacement for `x += (target - x) * k`.
* lambda is "how many e-foldings per second". To port a 60 Hz factor k: lambda = -60 * ln(1 - k).
*/
export const damp = (current, target, lambda, dt) => lerp(current, target, 1 - Math.exp(-lambda * dt));
export const lambdaFrom60 = k => -60 * Math.log(1 - k);
export const lambdaFromHalfLife = seconds => Math.LN2 / seconds;
/** The per-frame version found in 49 of our 69 cases — only correct at exactly 60 Hz. */
export const dampPerFrame = (current, target, k) => current + (target - current) * k;