运行时 · 31
帧循环
Frame loop · frame-loop一个循环更新持续运动;按秒计算 dt,并处理掉帧与后台恢复。
逐帧加固定像素会让速度随刷新率变化。按真实 dt 推进可以保持速度,但标签页后台十秒后一次性补上十秒,又会让元素瞬移。帧时长钳制与隐藏时暂停,是循环需要明确承担的两项工作。
公式
rawDt = (now − previous) / 1000 dt = min(rawDt, maxDt) position += speed × dt if hidden: pause; on return: reset previous
参数
| 本页默认 | 成熟实现 | |
|---|---|---|
| maxDt | 40 ms | Motion maxElapsed = 40 ms |
| speed | 110 px/s | — |
| visibility | 隐藏时暂停 | — |
怎样做好
- 持续值交给同一个 rAF 更新,让位置和速度使用同一份帧时长。www-motion/rewrite/lib/motion-kit.js:290–306
- 把异常大的 dt 钳住。Motion 的帧循环把 elapsed 限在 40 ms;我们的案例运行时也限制一次最多补 3 个 60 Hz 帧。motion/packages/motion-dom/src/frameloop/batcher.ts:6,48; www-motion/rewrite/lib/case-runtime.js:22–27
- 隐藏时停止,恢复时重置上次时间;别把后台停留算成一帧。www-motion/rewrite/lib/case-runtime.js:28–40
在我们的案例里
frame-loop、dt-clamp 与 visibility-gate 合计覆盖 64 个案例;其中 19 个有帧时长钳制,53 个有可见性门控。rewrite/lib/case-runtime.js 已给循环传入钳制后的步长,并在标签页隐藏时停掉动画。
49 / 69 个案例 · 有逐帧积分(未乘 dt)来自 Codex 逐案质量标记(按关键词归类,近似值)
出现在 64 个案例
01Momentum Slider02Drag Gallery03Formation04Cardwall05Riffle06Revolver07Rotunda08Iris09Lens Bloom10Frieze11Aperture12Folio13Cinema Reel14Stream Showcase15Hardback16Picasso Ecommerce17Assembly18Fathom
+46 更多
19Emulsion20Portfolio Photographer21Reverie22Boutique23Nova24Alive25Shape Trail26Membrane27Liquid Type28Filament29Pixels30Cipher32Cutout33Crema34Aster35Spring36Benchmark37Amour38Fluid Gallery39Marathon42Isle43Pantheon44Atlas45Aurora46Rooster Rush47Overworld48Moments49Burst50Dimension51Morph52Meow53Relay54Lumen55Serene56Fleur57Atrium58Skyward59Helix60Crate61Prism62Flashlight63Viewfinder64Sand66Tunnel67Reel69Loupe
参考实现 · basics.js
/**
* The single writer: one rAF loop owns every continuous value.
* dt is in seconds and clamped (a backgrounded tab returns with a 10 s frame);
* the loop pauses while the page is hidden.
*/
export function frameLoop(update, { maxDt = 0.04 } = {}) {
let raf = 0, last = 0, running = false;
const frame = now => {
const dt = last ? Math.min((now - last) / 1000, maxDt) : 1 / 60;
last = now; update(dt, now / 1000);
raf = requestAnimationFrame(frame);
};
const start = () => { if (!running) { running = true; last = 0; raf = requestAnimationFrame(frame); } };
const stop = () => { running = false; cancelAnimationFrame(raf); };
const onVis = () => (document.hidden ? stop() : start());
document.addEventListener('visibilitychange', onVis);
if (!document.hidden) start();
return { start, stop, destroy() { stop(); document.removeEventListener('visibilitychange', onVis); } };
}