Files
Crystal/src/input/bindings.ts
T
2026-09-23 09:23:29 +02:00

166 lines
4.0 KiB
TypeScript

import { type Button } from "./types";
import { InputController } from "./controller";
const KEYS: Record<string, Button> = {
ArrowUp: "up",
ArrowDown: "down",
ArrowLeft: "left",
ArrowRight: "right",
KeyX: "a",
KeyZ: "b",
Enter: "start",
ShiftLeft: "select",
ShiftRight: "select",
};
export function bindControls(
root: HTMLElement,
input: InputController,
active: () => boolean,
unlock: () => void,
) {
const abort = new AbortController();
const options = { signal: abort.signal };
const buttons = [
...root.querySelectorAll<HTMLButtonElement>("[data-game-button]"),
];
const refresh = () =>
buttons.forEach((el) => {
const pressed = input.isPressed(el.dataset.gameButton as Button);
el.classList.toggle("pressed", pressed);
el.setAttribute("aria-pressed", String(pressed));
});
const release = () => {
input.releaseAll();
refresh();
};
for (const el of buttons) {
const focusedSource = `focused:${el.dataset.gameButton}`;
el.addEventListener(
"keydown",
(event) => {
if (!active() || !["Space", "Enter"].includes(event.code)) return;
event.preventDefault();
unlock();
input.press(focusedSource, el.dataset.gameButton as Button);
refresh();
},
options,
);
el.addEventListener(
"keyup",
(event) => {
if (!["Space", "Enter"].includes(event.code)) return;
event.preventDefault();
input.release(focusedSource);
refresh();
},
options,
);
el.addEventListener(
"blur",
() => {
input.release(focusedSource);
refresh();
},
options,
);
el.addEventListener(
"pointerdown",
(event) => {
if (!active() || (event.pointerType === "mouse" && event.button !== 0))
return;
event.preventDefault();
unlock();
el.setPointerCapture(event.pointerId);
input.press(
`pointer:${event.pointerId}`,
el.dataset.gameButton as Button,
);
refresh();
},
options,
);
// Slide between D-pad directions without lifting the finger.
el.addEventListener(
"pointermove",
(event) => {
if (!el.hasPointerCapture(event.pointerId) || !el.closest(".dpad"))
return;
const target = document
.elementFromPoint(event.clientX, event.clientY)
?.closest<HTMLElement>("[data-game-button]");
if (target?.closest(".dpad"))
input.press(
`pointer:${event.pointerId}`,
target.dataset.gameButton as Button,
);
else input.release(`pointer:${event.pointerId}`);
refresh();
},
options,
);
for (const name of [
"pointerup",
"pointercancel",
"lostpointercapture",
] as const) {
el.addEventListener(
name,
(event) => {
input.release(`pointer:${event.pointerId}`);
refresh();
},
options,
);
}
el.addEventListener(
"contextmenu",
(event) => event.preventDefault(),
options,
);
}
window.addEventListener(
"keydown",
(event) => {
if (!active() || event.ctrlKey || event.metaKey || event.altKey) return;
if (
(event.target as HTMLElement)?.closest(
"input, textarea, select, button, a, [contenteditable]",
)
)
return;
const button = KEYS[event.code];
if (!button) return;
event.preventDefault();
unlock();
input.press(`key:${event.code}`, button);
refresh();
},
options,
);
window.addEventListener(
"keyup",
(event) => {
input.release(`key:${event.code}`);
refresh();
},
options,
);
window.addEventListener("blur", release, options);
document.addEventListener(
"visibilitychange",
() => {
if (document.hidden) release();
},
options,
);
return {
release,
dispose: () => {
release();
abort.abort();
},
};
}