joymap
Version:
A Gamepad API wrapper and mapping tool.
153 lines (127 loc) • 4.15 kB
text/typescript
import {
noop,
map,
isFunction,
find,
filter,
difference,
forEach,
includes,
compact,
} from 'lodash/fp';
import { getRawGamepads, gamepadIsValid } from './common/utils';
import { RawGamepad, JoymapParams } from './types';
import { BaseModule } from './baseModule/base';
import { QueryModule } from './queryModule/query';
import { StreamModule } from './streamModule/stream';
import { EventModule } from './eventModule/event';
interface JoymapState {
onPoll: () => void;
autoConnect: boolean;
gamepads: RawGamepad[];
modules: AnyModule[];
}
export type AnyModule = BaseModule['module'] | QueryModule | StreamModule | EventModule;
export type Joymap = ReturnType<typeof createJoymap>;
export default function createJoymap(params: JoymapParams = {}) {
let animationFrameRequestId: number | null = null;
const isSupported = navigator && isFunction(navigator.getGamepads);
const state: JoymapState = {
onPoll: params.onPoll || noop,
autoConnect: params.autoConnect !== false,
gamepads: [],
modules: [],
};
const joymap = {
isSupported: () => isSupported,
start: () => {
if (isSupported && animationFrameRequestId === null) {
joymap.poll();
if (state.autoConnect) {
forEach((module) => {
if (!module.isConnected()) {
const padId = joymap.getUnusedPadId();
if (padId) {
module.connect(padId);
}
}
}, state.modules);
}
const step = () => {
joymap.poll();
animationFrameRequestId = window.requestAnimationFrame(step);
};
animationFrameRequestId = window.requestAnimationFrame(step);
}
},
stop: () => {
if (animationFrameRequestId !== null) {
window.cancelAnimationFrame(animationFrameRequestId);
animationFrameRequestId = null;
}
},
setOnPoll: (onPoll: () => void) => {
state.onPoll = onPoll;
},
setAutoConnect: (autoConnect: boolean) => {
state.autoConnect = autoConnect;
},
getGamepads: () => state.gamepads,
getModules: () => state.modules,
getUnusedPadIds: () =>
compact(
difference(
map('id', state.gamepads),
map((module) => module.getPadId(), state.modules),
),
),
getUnusedPadId: () => {
const usedIds = map((module) => module.getPadId(), state.modules);
const gamepadIds = map('id', state.gamepads);
return find((id) => !includes(id, usedIds), gamepadIds);
},
addModule: (module: AnyModule) => {
state.modules.push(module);
if (state.autoConnect && !module.getPadId()) {
const padId = joymap.getUnusedPadId();
if (padId) {
module.connect(padId);
}
}
},
removeModule: (module: AnyModule) => {
state.modules = filter((m) => m !== module, state.modules);
module.destroy();
},
clearModules: () => {
forEach((module) => joymap.removeModule(module), state.modules);
},
poll: () => {
state.gamepads = filter(gamepadIsValid, getRawGamepads()) as RawGamepad[];
forEach((module) => {
if (state.autoConnect && !module.getPadId()) {
const padId = joymap.getUnusedPadId();
if (padId) {
module.connect(padId);
const pad = find({ id: module.getPadId() }, state.gamepads) as RawGamepad | undefined;
if (pad) {
module.update(pad);
}
}
} else {
const gamepad = find({ id: module.getPadId() }, state.gamepads) as RawGamepad | undefined;
if (gamepad) {
if (!module.isConnected()) {
module.connect();
}
module.update(gamepad);
} else if (module.isConnected()) {
module.disconnect();
}
}
}, state.modules);
state.onPoll();
},
};
return joymap;
}