Skip to content

Instantly share code, notes, and snippets.

@cuylerstuwe
Last active July 13, 2019 15:34
Show Gist options
  • Select an option

  • Save cuylerstuwe/98cd58cfcee694e3bbc08adc3d925080 to your computer and use it in GitHub Desktop.

Select an option

Save cuylerstuwe/98cd58cfcee694e3bbc08adc3d925080 to your computer and use it in GitHub Desktop.
// Event-Driven Web Controllers Library
// Version: 1.6
// by Cuyler Stuwe (salembeats)
//
// Purpose:
//
// Simplify usage of responding to controller button change events, to utilize controllers as simple work productivity tools (where ease-of-use trumps performance).
//
// Instructions:
//
// Bind a window listener for the "vilrosButton" event, for example:
//
// window.addEventListener("controllerButton", e => {
// ("Controller", e.detail.controllerIndex, "button", e.detail.button, "pressed?", e.detail.pressed);
// });
//
// Changelog:
// 1.6 Generate starting button-press events from "gamepad connected" events, initialize library by default on include.
// 1.51 Fixed missing comma that caused crash.
// 1.5 Added controller nicknames.
// 1.42 Added mappings for the buttons under the joysticks for the default Xbox mapping.
// 1.41 Removed extraneous console log.
// 1.4 Watching for analog axes as well as buttons.
// 1.3 Looking at values rather than pressed state for comparisons, to generate streams of events that respect the full range of analog triggers.
// 1.2 Extracted mappings so they can be initialized in a constructor.
// 1.1 Genericized to respond well to an additional controller (Logitech F310).
// 1.0 Initial version. Works with Vilros SNES controllers.
const defaultControllerTypeNicknames = {
"USB Gamepad (STANDARD GAMEPAD Vendor: 0079 Product: 0011)": "vilros snes",
"Xbox 360 Controller (XInput STANDARD GAMEPAD)": "xinput"
};
const defaultAllControllersIndexToButtonMaps = {
"USB Gamepad (STANDARD GAMEPAD Vendor: 0079 Product: 0011)": {
0: "b",
1: "a",
2: "y",
3: "x",
4: "l",
5: "r",
8: "select",
9: "start",
12: "up",
13: "down",
14: "left",
15: "right"
},
"Xbox 360 Controller (XInput STANDARD GAMEPAD)": {
0: "a",
1: "b",
2: "x",
3: "y",
4: "l",
5: "r",
6: "l2",
7: "r2",
8: "select",
9: "start",
10: "left joystick",
11: "right joystick",
12: "up",
13: "down",
14: "left",
15: "right"
}
};
const defaultAllControllersIndexToAxisMaps = {
"Xbox 360 Controller (XInput STANDARD GAMEPAD)": {
0: "left x",
1: "left y",
2: "right x",
3: "right y"
}
};
function InitializeControllerEventLoop(
allControllersIndexToButtonMap = defaultAllControllersIndexToButtonMaps,
allControllersIndexToAxisMaps = defaultAllControllersIndexToAxisMaps,
controllerTypeNicknames = defaultControllerTypeNicknames
) {
/* // UNUSED -- FOR INVERTING INDEX=>BUTTON-NAME MAPS (TO CREATE BUTTON-NAME=>INDEX MAPS).
const invertKeyValuePairs = obj => (
Object.keys(obj)
.map(key => ({
[obj[key]]: key
}))
.reduce((acc, pair) => ({
...acc,
...pair
}), {})
);
const allControllersButtonToIndexMap = (
Object.keys(allControllersIndexToButtonMap)
.map(controllerTypeName => ({
[controllerTypeName]: invertKeyValuePairs(allControllersIndexToButtonMap[controllerTypeName])
}))
.reduce((acc, obj) => ({
...acc,
...obj
}), {})
);
*/
const allControllersIndexToAxisGroupArrayMaps = (
Object.keys(allControllersIndexToAxisMaps)
.map(controllerTypeName => {
const indexToAxisMap = allControllersIndexToAxisMaps[controllerTypeName];
const controllerIndexToAxisGroupMap = Object.keys(indexToAxisMap).reduce((acc, idxKey) => {
const keyWords = indexToAxisMap[idxKey].split(" ");
if(keyWords.includes("left")) {
acc.left.push(+idxKey);
}
else if(keyWords.includes("right")) {
acc.right.push(+idxKey);
}
return acc;
}, {left: [], right: []});
return {
[controllerTypeName]: controllerIndexToAxisGroupMap
};
})
.reduce((acc, obj) => ({
...acc,
...obj
}), {})
);
const allControllersIndexToAxisDirectionMaps = (
Object.keys(allControllersIndexToAxisMaps)
.map(controllerTypeName => {
const indexToAxisMap = allControllersIndexToAxisMaps[controllerTypeName];
const xyMap = Object.keys(indexToAxisMap).reduce((acc, idxKey) => {
const keyWords = indexToAxisMap[idxKey].split(" ");
if(keyWords.includes("x")) {
acc[idxKey] = "x";
}
else if(keyWords.includes("y")) {
acc[idxKey] = "y";
}
return acc;
}, {});
return {
[controllerTypeName]: xyMap
};
})
.reduce((acc, obj) => ({
...acc,
...obj
}), {})
);
let formerPolledStatus;
let latestPolledStatus;
const frameCycle = timestamp => {
formerPolledStatus = {...latestPolledStatus};
latestPolledStatus = [...navigator.getGamepads()];
latestPolledStatus.forEach((controllerStatus, controllerIdx) => {
const formerControllerStatus = formerPolledStatus[controllerIdx];
if(!(controllerStatus && formerControllerStatus)) { return; }
controllerStatus.buttons.forEach((button, buttonIdx) => {
const formerButtonValue = formerControllerStatus.buttons[buttonIdx].value;
if(button.value !== formerButtonValue) {
const controllerIndexToButtonMap = allControllersIndexToButtonMap[controllerStatus.id];
window.dispatchEvent(new CustomEvent("controllerbutton", {
detail: {
controllerTypeNickname: controllerTypeNicknames[controllerStatus.id],
controllerIndex: controllerIdx,
buttonIndex: buttonIdx,
button: controllerIndexToButtonMap[buttonIdx],
pressed: button.pressed,
value: button.value
}
}));
}
});
const controllerIndexToAxisGroupArrayMap = allControllersIndexToAxisGroupArrayMaps[controllerStatus.id];
Object.keys(controllerIndexToAxisGroupArrayMap || {}).forEach(controllerIndexToAxisGroupName => {
const axisIndexGroup = controllerIndexToAxisGroupArrayMap[controllerIndexToAxisGroupName];
const isAxisGroupChanged = axisIndexGroup.some(axisIndex => {
const formerAxisValue = formerControllerStatus.axes[axisIndex];
const currentAxisValue = controllerStatus.axes[axisIndex];
return (currentAxisValue !== formerAxisValue);
});
if(isAxisGroupChanged) {
const axisGroupNewXyPositions = axisIndexGroup.reduce((acc, idxVal) => {
const indexToAxisDirectionMap = allControllersIndexToAxisDirectionMaps[controllerStatus.id];
return {
...acc,
[indexToAxisDirectionMap[idxVal]]: controllerStatus.axes[idxVal]
};
}, {});
window.dispatchEvent(new CustomEvent("controlleraxis", {
detail: {
controllerTypeNickname: controllerTypeNicknames[controllerStatus.id],
controllerIndex: controllerIdx,
joystick: controllerIndexToAxisGroupName,
x: axisGroupNewXyPositions.x,
y: axisGroupNewXyPositions.y
}
}));
}
});
});
window.requestAnimationFrame(frameCycle);
};
frameCycle();
window.addEventListener("gamepadconnected", (event) => {
for(const [buttonIdx, button] of event.gamepad.buttons.entries()) {
if(button.pressed) {
window.dispatchEvent(new CustomEvent("controllerbutton", {
detail: {
controllerTypeNickname: controllerTypeNicknames[event.id],
controllerIndex: event.gamepad.index,
buttonIndex: buttonIdx,
button: allControllersIndexToButtonMap[event.gamepad.id][buttonIdx],
pressed: button.pressed,
value: button.value
}
}));
}
}
});
}
InitializeControllerEventLoop();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment