定義
chord(コード) は、鍵盤で複数の音を同時に弾く和音のように、複数のキーを一緒に押してひとつの動作を作る入力です。このドキュメントの chord 収集ウィンドウは、最初のキーイベントが届いてもすぐに結果を決めず、短い時間のあいだに一緒に入ってくるキーを集めてから、まとめて確定する方法です。
なぜ必要か
ブラウザには「同時に押した」という概念がありません。W と D を一緒に押しても、keydown イベントはキーごとに別々に、順番をもって届きます。ゲームのように、画面を描くループ(requestAnimationFrame)と、ルールを一コマ進める固定間隔の計算(シミュレーションステップ)が別々に回る構造では、そのイベントのあいだにフレームやステップが割り込むことがあります。
そこで次のようなバグが起こります。同じ方向キーを素早く二回押すとその方向にダッシュする操作で、最初のキーイベントで方向をすぐに確定して次のステップで使うと、直後に届いた二つ目のキー(斜めを作るキー)は合流できません。二つのキーを交互に押すと、片方のキーがすでに押されたままになっているため偶然斜めになり、見た目には同じ操作なのに結果が違って見えます。
動作原理
時間 → t0: keydown W (ダブルタップ成立) t0+Δ: keydown D
├──────── 収集ウィンドウ (例: 数十 ms, 延長しない) ────────┤
└→ シミュレーションステップで W+D の斜めとして一度だけ消費
- 最初のイベントで確定しません。 ダブルタップが成立する最初のキーで収集ウィンドウを開きます。
- ウィンドウ内の別の軸のキーを補います。 垂直(W/S)のキーと水平(A/D)のキーが一つのウィンドウに入ったら、方向を合成します。
- ウィンドウは延長しません。 キーが来るたびにウィンドウを延ばすと、遅延に上限がなくなります。最初のキーでだけ開始し、固定の長さで終わらせます。
- 消費はシミュレーションステップで一度だけ行います。 描画フレームで消費すると、描画とシミュレーションがずれたときに二回実行されたり抜けたりします。消費したらバッファを空にします。
- 状態が変わったら空にします。 ウィンドウがフォーカスを失ったとき(blur)や、一時停止、再開始で、バッファと「最初のタップを押した記録」を消します。
実務での適用
const CHORD_WINDOW_MS = 50; // 体感できる遅延のない短い上限。固定の長さ。
let pending: { firstKey: string; startedAt: number; axes: Set<"x" | "z"> } | null = null;
window.addEventListener("keydown", (event) => {
if (!isDoubleTap(event.code)) return;
pending ??= { firstKey: event.code, startedAt: performance.now(), axes: new Set() };
pending.axes.add(axisOf(event.code)); // ウィンドウ内に入ってきたキーの軸を集める
});
function onSimulationStep(now: number) {
if (pending && now - pending.startedAt >= CHORD_WINDOW_MS) {
dash(directionFrom(pending.axes)); // 一度だけ消費する
pending = null;
}
}
function resetInput() {
pending = null; // blur、pause、restart で呼び出す
}
テストは、同じ組み合わせを二つのキー順の両方で、さらに二つ目の keydown のあいだにフレームを挟んで書きます。「交互に押す」だけを検証すると、この回帰は捕まえられません。
トレードオフ
| 選択 | 長所 | コスト |
|---|---|---|
| 最初のイベントで即確定 | 遅延ゼロ | 一緒に押したキーを逃すことがあります |
| 短い収集ウィンドウ | 同時入力を安定して合成できる | 単一キーの入力も、ウィンドウの長さだけ遅れて実行されます |
| 長い収集ウィンドウ | ゆっくりした同時入力も拾える | 操作が鈍く感じられます |
使うべきでない場合
- キーひとつで足りる動作には、収集ウィンドウは必要ありません。不要な遅延が増えるだけです。
- 精密な入力が必要な格闘ゲームのようなものは、フレーム単位の入力バッファの方針を別に設計する必要があります。このドキュメントの方式はそのままでは使いにくいです。
- この方式を、ほかの入力方式(ゲームパッド、タッチ)にそのまま一般化する根拠はありません。
よくある間違い
- 「同時に押した」を一つのイベントだと思い込む。 キーごとに別々のイベントが届きます。
- キーが来るたびにウィンドウを延長する。 遅延に上限がなくなります。
- リセットの経路を忘れる。 blur や pause のあとに残った古い入力が、次の動作を汚染します。
- 交互に押すテストだけを書く。 同じ操作でも、イベントの順序が違えば別のコードパスです。
関連概念
- ime-composition-commit-boundary — 複数の入力が集まってひとつの意味になるとき、確定のタイミングを先送りする、同じ系統の問題
- pointer-events-touch-gesture-handling — キャンセルやリセットの経路で状態を空にする原則
- test-oracle — 期待値を実装ではなく、正しい動作から得るという考え方