ZMKトラックボールに“iOS風 慣性スクロール”を実装した話
自作キーボードでトラックボールを使っていると、
ポインタ操作は快適なのに スクロールだけ妙に無機質 に感じることがあります。
特に ZMK + PMW3610 系構成では、
- スクロール量は足りている
- 速度も出せる
- 実用上困らない
それでもどこか、
「気持ちよくない」
と感じていました。
そこで今回、ZMK の input processor(※)として、
トラックボール向け慣性スクロールモジュール
を実装しました。
これは加速度を足すのではなく、ボールを離したあとも自然に流れて止まるスクロールを目指して実装してます。
(GIFのサイズ制限のためfps落としてますが、実際には更に滑らかです)
かなり満足度の高い仕上がりになったので、その考え方や実装方針をまとめます。
何が不満だったのか
通常のスクロールは、指を止めた瞬間に止まります。
これは機械的には正しいのですが、トラックボールのように
- 指で勢いをつける
- フリックする
- アナログ的に入力する
デバイスでは、少し不自然に感じます。
イメージとしては、
- 指では“投げた”のに
- 画面側は“即停止する”
というズレです。
加速度では解決しない
ZMKでトラックボールのスクロールを快適にしようとすると、
多くの人はまず 加速度 を考えます。
たしかに、
- 速く回したら大きくスクロールする
- 少ない回転で長距離移動できる
というメリットはあります。
ただ、今回欲しかったのはそこではありません。
欲しかったのは、
止まり際まで気持ちいいこと
です。
つまり必要だったのは加速度ではなく、
慣性スクロール でした。
目指したのは iOS っぽい感触
スマホ、とくに iPhone のスクロールって妙に自然です。
- 指を離した後も少し流れる
- 速度に応じて減衰する
- 止まり際に嫌な引っ掛かりがない
今回のモジュールでは、そういった
“触って気持ちいい側のスクロール感”
を最優先事項と意識して調整しました。
ただし、タッチパッドと同じ実装にはできない
ここでよくある誤解として、
「スマホやタッチパッドの慣性スクロールと同じ仕組みにすればいいのでは?」と思われがちです。
しかし、トラックボールではそれがそのまま成立しません。
理由はシンプルで、指を離した瞬間が装置側から分からない からです。
タッチパッドは、
- 触れている
- 離した
が明確なので、離した瞬間の速度を元にそのまま慣性へ移れます。
一方トラックボールは、
- 触れ続けて操作するのが普通
- 離したことを直接検知できない
- 離した後もボールが物理的に回り続けて入力が出る(ガチの慣性)
という構造です。

そのため、
- 動きが緩んだら離したっぽいと推測する
- 慣性開始直後のボールの余韻入力を吸収する
- 短いフリック誤爆を防ぐため発動条件を設ける
といった、タッチパッドには不要な補助ロジックが必要になります。
なぜドライバ改造ではなく input processor なのか
PMW3610 ドライバ側を改造する方法もあります。
ただ今回は、あえて input processor 層 で実装しました。
理由はシンプルで、
- ZMK本体への追従が楽
- ドライバ fork 管理をしたくない
- 他センサー構成にも応用しやすい
- 導入しやすい
からです。
理論上はドライバ側の方が細かく制御しやすい面もありますが、
実用品としてのバランスはこちらの方が良いと判断しました。
軸自動判定という「底なし沼」を捨てた理由
スクロール用レイヤーを1枚だけ用意し、入力から「縦スクロールか横スクロールか」を判定する 自動軸判定方式。
一見スマートですが、実際のトラックボール操作とは相性がよくありません。
ここで「そもそも軸を判定(ロック)せず、そのまま2Dでスクロールさせればいいのではないか?」という疑問が出るかもしれませんが、話はそう単純ではありません。
1. 軸を判定しないと「ノイズ」が乗る
もちろん、軸を固定しない自由な2Dスクロールが適している場面もあります。
地図アプリの広域操作やデザインワークなど、全方位にシームレスに動かしたい「特殊な用途」では、軸を縛らない方が圧倒的に快適です。
しかし、日常のWeb閲覧やコード書きなど、
「ユーザーの意図(縦に動かしたい・横に動かしたい)がハッキリしている操作」
においては、話が変わります。
特に「慣性」を効かせる場合、自由2Dでは離し際のわずかな角度のズレがそのまま「斜め方向への流れ」となって現れます。
これではテキストを追う際に画面がガタつき、精度の低い体験になってしまいます。
ですので、快適なスクロール体験には軸の固定自体は必要なわけです。
※ オプションとして軸を固定しない自由2Dスクロールはちゃんと実装してます。

2. 自動軸判定は「後出しジャンケン」
では横揺れを防ぐために、ある程度の初動スクロールで軸を勝手に判定しロックする処理を入れるとどうなるか。
- 45度付近で望まない方向にロックされ、ストレスが溜まる。
- ロック解除の判定待ちでレスポンスが遅れる。
- そもそもユーザーの意図を後追いで推測する構造は変わらない(初動の反応が悪くなる)。

※ 実は真に致命的なのは "判定" することではなく、その判定によって起こした変化を操作者の意図通りに "解除" することだったりします。
これは多分潜った人全員が最終的に行き着く答えです。
3. 「入力にない情報」は復元できない
根本的な問題は、「ユーザーの頭の中にある『今は縦に動かしたい』という純粋な意図」は、物理デバイスの出力信号には100%含まれていないということです。
つまりこれらは「頑張って直せるバグ」ではなく、そもそも入力に存在しない情報を読み取ろうとしていることから来る構造的な限界です。
もしかすると他にも「挑戦したけど断念した、妥協した」という方がいらっしゃるかもしれませんが、ここは最初から不可能な領域なので安心していいです。
どんな天才が実装してもトラックボールの構造が現在のままである限り、完全完璧な自動軸判定は永久に実現しません。
理由は単純で、ユーザーの意図が入力信号に含まれていないからです。
だから「意図を先に確定させる」
実際にはユーザー側に、「今は縦スクロールしたい」「今は横スクロールしたい」という意図が最初からあります。
であれば、その意図を後から推測するより、先に意図を宣言してもらう 方が明快です。
具体的には、
- 縦スクロールレイヤー
- 横スクロールレイヤー
を分けて運用します。これにより、
- 軸誤判定がゼロになる
- 慣性発動条件が安定する
- 調整対象を絞れる
といったメリットが得られます。

つまり「リモコンを既に握っているのにアレクサにテレビつけてと言ってる」ような意味不明な状況を見切ったわけです。
そうすることで、以下の圧倒的な利点が生まれました。
曖昧な角度でもスクロール量を安定させることが可能になる
この方式の最大の恩恵は、トラックボールを曖昧な角度で回しても、スクロール量が安定すること です。
既存のトラックボールの価値を大きく引き上げかねないこの機能は、軸を確定させ、なおかつ自動判定を捨てなければ実現できません。
一般的な軸を確定させない方式や自動軸判定方式では、
- 少し角度が変わるだけで縦横判定が揺れる
- 縦成分と横成分が毎回変化する
- 同じつもりで回してもスクロール量がばらつく
といった問題が起こりやすくなります。
一方、このモジュールはあらかじめ縦スクロール・横スクロールの意図を確定させているため、
入力角度が多少斜めになっても、出力されるスクロール量は一定です。
つまり、
- 指の角度を神経質に合わせなくてよい
- ラフに回しても狙った量だけ進む
- 毎回同じ感覚で操作できる
という、実用上非常に大きなメリットがあります。

数値的な正確さだけでなく、
人間が雑に使っても気持ちよく再現性が高い ことを重視した設計です。
実は「ドライバを改造すれば簡単に勝てる」わけではない
ここは誤解されやすい点です。
たしかにドライバ層の方が、
- 生入力に近い値を取れる
- より細かいタイミング制御ができる
- 理論上の制御自由度が高い
というアドバンテージがあります。
ただし、それだけでスクロール体験が良くなるわけではありません。
重要なのは、
人間がどう感じるか
です。
たとえば慣性スクロールでは、
- いつ発動するか
- どの勢いで始まるか
- 中盤の伸び方
- 止まり際の減速感
- 逆入力した時の収まり方
など、多くの要素が絡みます。
つまり、
低レイヤーにいること ≠ 気持ちいいスクロールになること
です。
実際いくつものドライバー改造方式の実装を見ましたが、少なくとも私が確認した範囲では、この input processor 実装と張り合えるところまで体感品質を詰めたものは見つかりませんでした。
むしろ到達が難しい領域もある
今回感じたのは、
生半可なドライバ改造より、
徹底的にチューニングした input processor の方が体感品質で上回り得る ということです。
理由はシンプルで、
ドライバ改造は技術的に面白くても、
- そこそこ動いた段階で満足しやすい
- 保守コストが高い
- UX調整まで到達しにくい
からです。
一方で今回は、
- 実機で何度も試す
- 違和感を潰す
- デフォルト設定を詰める
- 日常利用で評価する
という、人間側の品質調整に相当な時間を使いました。
結果として、
ドライバ層にいかなくても、実用上違いを認識できないレベルの高い完成度まで到達した
と感じています。
実装で詰めた部分
単純に「入力が止まったら一定時間流す」では、かなり不自然になります。
今回詰めたのは主にこのあたりです。
発動条件
意図しない微小操作で慣性が出ないように、
- 速度
- 移動量
- 継続入力
などを見て発動条件を調整しました。
これは低速・短距離のスクロールでは、むしろ慣性が無い方が使いやすい場面もあるためです。
そこで、発動用の閾値(start と move)を設け、軽い送り操作では慣性が乗らないようにしています。
これがあるお陰で気持ちのいい慣性を取り入れつつ、細かい操作もしやすくなります。
減衰カーブと終端の質感
単一減衰だけでは、小さなフリックが必要以上に長く残ったり、終端がだらついたりすることがあります。
そのため、減衰率だけでなく終端側の質感も調整できるようにし、低速域ではクーロン摩擦も使って自然に止まるよう詰めました。
逆入力キャンセル
逆方向に触れたら素直に止まるよう調整しました。
デフォルト設定
多くの人は設定を細かく触りません。
なので、
入れた瞬間に気持ちいい
を重視しました。
ただし、先述した通り発動させたくない移動量と移動速度は好みに依るところが大きいため、実際に動かしながらオプションで調整してください。
導入方法
west.yml にプロジェクトを追加し、通常どおりビルドするだけで導入できます。
west.yml に追加
manifest:
remotes:
- name: mjmjm0101
url-base: https://github.com/mjmjm0101
projects:
- name: zmk-input-processor-scroll-inertia
remote: mjmjm0101
revision: main
既存の projects: 配下へ追記してください。
overlay で設定
例として、縦スクロール用レイヤーに適用する場合は以下のように設定します。
/ {
scroll_inertia_v: scroll_inertia_v {
compatible = "zmk,input-processor-scroll-inertia";
#input-processor-cells = <0>;
axis = <1>; /* 縦スクロール */
layer = <4>; /* 対象レイヤー */
scale = <4>; /* scroll_scalerと合わせる */
scale-div = <675>; /* scroll_scalerと合わせる */
};
};
&trackball_listener {
scroller_v {
layers = <4>;
input-processors = <
&zip_y_scaler (-1) 1
&zip_xy_to_scroll_mapper
&scroll_inertia_v
&zip_scroll_scaler 4 675
>;
};
};
ポイント
- axis = <1> : 縦スクロール
- axis = <2> : 横スクロール
- axis = <0> : 自由2Dスクロール
縦スクロール用・横スクロール用でレイヤーを分けると、それぞれ個別に慣性のかかり方などを調整可能です。
詳しい内容はREADMEを参照してください。
調整回数は…めっちゃ多かったです
体感調整なので、机上では終わりません。
実機で試してはビルドし直し、また触って確認……を繰り返し、
かなりの回数(200回は余裕で超えてます)ビルドしました。
でもその結果、
今さら外したくないレベル
まで来ました。
一度使うと戻りにくい
実際に比較のため一度無効化したのですが、めちゃくちゃ違和感がありました。
以前のスクロールが悪いというより、
- 急に止まりすぎる
- 味気ない
- デジタルっぽい
と感じるようになります。
こういう改善は、
ONだと気づきにくいけど、OFFにすると分かる
タイプかもしれません。
もし同じ不満があるなら
ZMK + トラックボール環境で、
- スクロールが少し味気ない
- 加速度では違う
- 指先の気持ちよさを上げたい
と思っている人には、自信を持ってオススメします。
GitHub
実装は GitHub で公開しています。
最後に
今回あらためて感じたのは、
入力デバイスは正確さだけではなく、感触も重要
ということでした。
数値上正しくても、触って気持ちよくなければ毎日は使いづらい。
逆に、少し気持ちいいだけで毎日の満足度が随分と変わります。
同じようにトラックボール付き自作キーボードを楽しんでいる方の参考になれば嬉しいです。
Discussion