InputCombination
InputCombination builds conditions from keyboard, mouse, and gamepad state. Conditions can represent a single control, a chord, alternatives, exclusions, or an ordered sequence.
import {
Input,
InputCombination
} from "@jolly-pixel/controls";
const canvas = document.querySelector("canvas");
if (!canvas) {
throw new Error("No canvas element found");
}
const input = new Input(canvas);
input.connect();
const save = InputCombination.all(
"ControlLeft.down",
"KeyS.pressed"
);
const moveUp = InputCombination.atLeastOne(
"KeyW.down",
"ArrowUp.down"
);
const selectWithoutShift = InputCombination.all(
InputCombination.mouse("left", "pressed"),
InputCombination.none("ShiftLeft.down")
);
function gameLoop() {
input.update();
if (save.evaluate(input)) {
console.log("Save");
}
if (moveUp.evaluate(input)) {
console.log("Move up");
}
if (selectWithoutShift.evaluate(input)) {
console.log("Select");
}
requestAnimationFrame(gameLoop);
}
gameLoop();Condition shape
Every factory result has the following structural interface:
interface InputCondition {
evaluate(input: Input): boolean;
reset(): void;
}evaluate() reads the current frame state from Input. reset() clears condition-owned progress. Atomic conditions have no progress to clear; composite conditions forward the reset to their children.
InputCondition and the atomic condition types are used by the public signatures but are not exported from the package root.
Action types
type CombinedInputState =
| "down"
| "pressed"
| "released";
type CombinedInputAction =
`${ExtendedKeyCode | MouseAction}.${CombinedInputState}`;"down" remains true while the control is held. "pressed" and "released" read the corresponding transition from the latest device update.
The combined action and state aliases are not exported from the package root. Factory overloads still validate them at compile time.
Atomic conditions
InputCombination.key()
InputCombination.key(
action: CombinedInputAction
): AtomicInput
InputCombination.key(
key: ExtendedKeyCode,
state?: CombinedInputState
): AtomicInputCreates a keyboard condition. The state defaults to "pressed".
InputCombination.key("Space");
InputCombination.key("KeyW", "down");
InputCombination.key("ShiftLeft.released");InputCombination.mouse()
InputCombination.mouse(
action: CombinedInputAction
): AtomicInput
InputCombination.mouse(
button: MouseAction,
state?: CombinedInputState
): AtomicInputCreates a mouse button or virtual wheel-button condition. The state defaults to "pressed".
InputCombination.mouse("left");
InputCombination.mouse("right", "down");
InputCombination.mouse("scrollUp.pressed");InputCombination.gamepad()
InputCombination.gamepad(
gamepad: GamepadIndex,
button: number | keyof typeof GamepadButton,
state?: CombinedInputState
): AtomicInputCreates a gamepad button condition. The state defaults to "pressed".
InputCombination.gamepad(0, "A");
InputCombination.gamepad(0, "LeftBumper", "down");The AtomicInput class returned by these methods is not exported from the package root.
Combined-action detection
InputCombination.isCombinedAction(
action: unknown
): action is CombinedInputActionReturns true when action is a string containing a period. It does not validate the key, mouse action, or state segments.
Composite conditions
Composite methods accept existing condition objects and combined-action strings:
type ConditionArgument =
| InputCondition
| CombinedInputAction;The composite signatures below use ConditionArgument as a local documentation alias. It is not exported.
A string passed directly to a composite is always converted with InputCombination.key(). Create mouse and gamepad conditions with their factories before passing them to a composite.
InputCombination.all()
InputCombination.all(
...conditions: ConditionArgument[]
): AllInputsReturns true when every child returns true during the same evaluation. This is used for chords and modifier guards. An empty all() condition returns true.
InputCombination.all(
"ControlLeft.down",
"ShiftLeft.down",
"KeyS.pressed"
);InputCombination.atLeastOne()
InputCombination.atLeastOne(
...conditions: ConditionArgument[]
): AtLeastOneInputReturns true when at least one child returns true. An empty condition returns false.
InputCombination.atLeastOne(
"KeyW.down",
"ArrowUp.down"
);InputCombination.none()
InputCombination.none(
...conditions: ConditionArgument[]
): NoneInputsReturns true when every child returns false. An empty condition returns true.
InputCombination.none(
"ShiftLeft.down",
"ShiftRight.down"
);Sequences
InputCombination.sequence(
...conditions: ConditionArgument[]
): SequenceInputs
InputCombination.sequenceWithTimeout(
timeoutMs: number,
...conditions: ConditionArgument[]
): SequenceInputsA sequence advances when its current child evaluates to true. Other active controls do not cancel progress. If the elapsed time since the previous matched child exceeds the timeout, progress returns to the first child before the current evaluation.
sequence() uses SequenceInputs.DefaultTimeout, which defaults to 100 milliseconds. sequenceWithTimeout() uses the supplied interval between matched steps.
After the final child matches, the sequence returns true once and resets its progress. Calling reset() also resets every child.
const konami = InputCombination.sequenceWithTimeout(
500,
"ArrowUp.pressed",
"ArrowUp.pressed",
"ArrowDown.pressed",
"ArrowDown.pressed"
);Concrete condition classes
The package root exports the composite implementations:
new AllInputs(conditions: InputCondition[])
new AtLeastOneInput(conditions: InputCondition[])
new NoneInputs(conditions: InputCondition[])
new SequenceInputs(
conditions: InputCondition[],
timeoutMs?: number,
now?: () => number
)SequenceInputs.DefaultTimeout is mutable and defaults to 100. The optional clock defaults to Date.now and allows deterministic sequence evaluation.
The factory methods are the practical entry point because InputCondition cannot currently be imported from the package root.