如何在100毫秒内让鼠标滚轮丝般顺滑:OpenLogi Smooth Scroll 运行时数学全剖析
【免费下载链接】OpenLogi⚡️A native, local-first alternative to Logitech Options+, written in Rust 🦀 — remap buttons, DPI, and SmartShift over HID++. No account, no telemetry.项目地址: https://gitcode.com/GitHub_Trending/op/OpenLogi
OpenLogi 是一个用 Rust 编写的原生本地优先工具,作为 Logitech Options+ 的无账号、无遥测替代方案。本文带你拆解它的Smooth Scroll(平滑滚动)运行时:一段 100 毫秒的三次缓动动画,如何被切成每 8 毫秒一帧的精确输出。无需任何数学背景,你只需要一个滚动的鼠标轮。🖱️
一、从"咔哒"到"丝滑":流水线总览
传统鼠标轮一次转动只产生一个离散的"滴答"(wheel tick)。OpenLogi 的平滑化流水线把它变成一条连续动画:
| 阶段 | 职责 | 关键文件 |
|---|---|---|
| 输入钩子 | 捕获物理滚轮事件,非阻塞提交 | worker.rs |
| 命令队列 | 容量 128 的有界同步通道 | 同上 |
| 滚动引擎 | 纯数学状态机,逐帧计算位置 | scroll.rs |
| 平台注入 | 生成操作系统的合成滚轮事件 | macos.rs |
一个值得注意的设计:只有"滴答式"滚轮输入会进入平滑模型。触摸板等像素级连续输入被明确拒绝(WheelDelta::try_from对Pixels类型返回错误),保持原生手感不被干扰。
二、核心数学:三次 smoothstep 缓动
每个滚动冲量(impulse)都会开启一个时长固定 100 毫秒的运动片段(ANIMATION_DURATION),位置由缓动函数求出(见 scroll.rs#L136-L141):
- 计算进度:
progress = 已过时间 ÷ 100ms,并夹取到 0~1 - 应用三次 smoothstep:
eased = p² × (3 − 2p) - 输出位置:
position = from + (target − from) × eased
smoothstep 的特点是首尾速度为零、中间最快——滚动有"加速—巡航—柔和收尾"的质感。用一个 1 个 tick 的冲量代入:
| 时刻 | progress | eased 值 | 累计滚动距离 |
|---|---|---|---|
| 25 ms | 0.25 | 0.15625 | 0.156 tick |
| 50 ms | 0.5 | 0.5 | 0.5 tick |
| 100 ms | 1.0 | 1.0 | 1.0 tick ✅ |
这些数字不是拍脑袋定的——它们就是 tests.rs 里synthetic_ratchet_trace测试断言的精确期望值,保证动画"分毫不差地停在对的位置"。
三、分阶段轮询:8 毫秒一帧是怎么驱动的
工作线程(名为openlogi-scroll)的循环非常巧妙,见 worker.rs#L352-L409:
- 它查询引擎中所有活跃片段的下一个帧截止时刻(
next_deadline); - 若无到期帧,就阻塞等待新命令;若有,则用
recv_timeout(距截止的剩余时间)等待; - 超时触发帧推进——也就是说,帧时钟不是定时器的 tick,而是"命令接收的超时",零额外线程。
帧间隔为 8 毫秒(约 125 Hz,FRAME_INTERVAL)。更妙的是位置由绝对时间求值,而非累计帧数:即使某次唤醒被延迟,动画既不会变慢、也不会被拉长,只是少发一帧。测试synthetic_delayed_frames直接验证了"密集唤醒"与"延迟唤醒"累计位移完全相同。
四、Retargeting:连拨滚轮不丢距离
快速连续滚动时,新滴答到达而旧片段还没播完。引擎不会打断重来,而是执行retarget(重新瞄准):
- 用旧片段在"新滴答到达时刻"求出现在位置;
- 新目标 = 旧目标 + 新冲量;
- 从当前位置开一个全新 100 ms 片段滑向新目标。
若新旧冲量恰好抵消(如 +1.0 后紧跟 −1.0),目标等于当前位置,引擎直接返回Finished(0)——一帧都不输出,测试synthetic_opposing_impulses_cancel_before_output断言帧列表为空。距离守恒由emitted计数器精确记账,任何时刻累计输出都不会多滚或少滚。
五、输出相位机:Began / Changed / Ended / Cancelled
前台应用只能看到一条滚动相位流(多个设备可以并行滚动,但相位生命周期全局共享且配平)。四个相位定义在 inject.rs#L384-L393:
| 相位 | 含义 | macOS 事件字段值 |
|---|---|---|
| Began | 动画第一帧 | 1 |
| Changed | 中间帧(含 retarget 后) | 2 |
| Ended | 收尾帧,携带精确修正 | 4 |
| Cancelled | 捕获源中途结束 | 8 |
在 macOS 上,每帧被合成为像素单位的连续滚动事件(1 tick = 10 点),并打上SCROLL_PHASE字段,让系统按真实的平滑滚动手势来消费——惯性、渲染管线全部对齐。
六、兜底设计:绝不阻塞输入回调
物理输入回调必须"快速失败"而不是等待:提交使用try_send,队列满(128 条)时立即返回 false,回调改用直通注入。两个设置(平滑开关 + 1~100 的垂直灵敏度)被编码进同一个字节(高位=开关,低 7 位=灵敏度),保证读取到的一致快照。
灵敏度换算为乘数:乘数 = 灵敏度 ÷ 14(默认档 14 即 1×)。HID++ 拇指滚轮的平滑路径则不套用主滚轮灵敏度,且取消操作精确到单次会话,绝不牵连其他设备。
七、动手体验:一分钟开启平滑滚动
在配置中打开开关即可(见 config.example.toml 与 docs/CONFIGURATION.md):
smooth_scroll = true vertical_scroll_sensitivity = 14想继续深挖?推荐阅读路线:
- 引擎数学:crates/openlogi-agent-core/src/runtime/scroll.rs
- 工作线程与队列:crates/openlogi-agent-core/src/runtime/scroll/worker.rs
- 全部数学用例(都是可运行的算法夹具):crates/openlogi-agent-core/src/runtime/scroll/tests.rs
- 平台注入层:crates/openlogi-inject/src/inject.rs
一句话总结:固定 100 ms 的 smoothstep 片段 + 绝对时间求值 + 距离守恒的 retarget,让"咔哒咔哒"的物理滚轮,在数学层面变成了一条精确、可取消、绝不超调的连续曲线。
【免费下载链接】OpenLogi⚡️A native, local-first alternative to Logitech Options+, written in Rust 🦀 — remap buttons, DPI, and SmartShift over HID++. No account, no telemetry.项目地址: https://gitcode.com/GitHub_Trending/op/OpenLogi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考