Repository: alvaromontoro/gamecontroller.js
Branch: master
Commit: a97889aeb587
Files: 28
Total size: 108.2 KB
Directory structure:
gitextract_1uoi5g71/
├── .babelrc
├── .editorconfig
├── .gitignore
├── .prettierrc
├── .travis.yml
├── changelog.md
├── dist/
│ └── gamecontroller.js
├── examples/
│ ├── example-0-connectivity.html
│ ├── example-3-buttons-and-joysticks.html
│ ├── example-4-snes-controller.html
│ ├── example-5-alvanoid.html
│ ├── example-6-multiplayer.html
│ ├── example-7-joystick-threshold.html
│ ├── example-8-vibration.html
│ ├── example-9-before-and-after.html
│ └── examples.css
├── license.md
├── package.json
├── readme.md
├── src/
│ ├── constants.js
│ ├── gamecontrol.js
│ ├── gamepad.js
│ ├── index.js
│ └── tools.js
└── tests/
├── gamecontrol.test.js
├── gamepad.test.js
├── mock.gamepads.js
└── tools.test.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"env": {
"test": {
"presets": [["@babel/preset-env"]]
}
}
}
================================================
FILE: .editorconfig
================================================
# top-most EditorConfig file
root = true
[*.md]
trim_trailing_whitespace = false
[*.js]
trim_trailing_whitespace = true
# Unix-style newlines with a newline ending every file
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
insert_final_newline = true
max_line_length = 100
================================================
FILE: .gitignore
================================================
.DS_Store
.github
node_modules
coverage
================================================
FILE: .prettierrc
================================================
{
"printWidth": 100,
"singleQuote": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "none",
"bracketSpacing": true,
"insertPragma": false,
"jsxBracketSameLine": false,
"semi": true,
"requirePragma": false,
"proseWrap": "preserve",
"arrowParens": "avoid"
}
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- 'node'
dist: trusty
cache:
directories:
- node_modules
================================================
FILE: changelog.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
## [v1.4.4] - 2019-12-08
### Updated
- Add tests
## [v1.4.3] - 2019-12-08
### Updated
- Dependency versions
## [v1.4.2] - 2019-09-03
### Added
- Bug fixes
- Update tests for gamecontrol
- Update readme file
## [v1.4.1] - 2019-09-02
### Added
- Bug fixes
- Tests to complete 95% statement coverage (missing event)
## [v1.4.0] - 2019-09-02
### Added
- After event for buttons and axe/joysticks
- Before event for buttons and axe/joysticks
- Refactor code to make library 20% smaller
## [v1.3.0] - 2019-09-01
### Added
- New vibration capability (experimental feature)
- New demo page with example for axe/joystick threshold
## [v1.2.1] - 2019-08-31
### Added
- Bug fix for Firefox (additional unexisting axe detected)
## [v1.2.0] - 2019-08-30
### Added
- Readable axe/joystick value properties (axeValues)
- Ability to set the axe/joystick sensitivity threshold (axeThreshold)
- Tests for new functionality
- New demo page with example for axe/joystick threshold
## [v1.1.2] - 2019-08-29
### Added
- Default value/existence check for gamepad to prevent potential bug
- Updated existing tests and added new ones for gamepad class
## [v1.1.1] - 2019-08-27
### Added
- Bug fix: when multiple gamepads are connected, all of them work indidually (instead of only the last one)
- Added a new demo for multiplayer (the classic Pong)
### Removed
- Deleted a couple of demos that were confusing (buttons and joysticks) and merged them into a single one
## [v1.1.0] - 2019-08-26
### Added
- Added missing aliases for directional event handlers
- New aliases for front buttons (R1, L1, R2, L2)
- Practical examples based on some from CodePen
## [v1.0.0] - 2019-08-25
### Added
- Basic functionality
- Event management for buttons and joystick/axes
- Examples
================================================
FILE: dist/gamecontroller.js
================================================
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./src/constants.js":
/*!**************************!*\
!*** ./src/constants.js ***!
\**************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"MESSAGES\": () => (/* binding */ MESSAGES)\n/* harmony export */ });\nconst MESSAGES = {\n ON: 'Gamepad detected.',\n OFF: 'Gamepad disconnected.',\n INVALID_PROPERTY: 'Invalid property.',\n INVALID_VALUE_NUMBER: 'Invalid value. It must be a number between 0.00 and 1.00.',\n INVALID_BUTTON: 'Button does not exist.',\n UNKNOWN_EVENT: 'Unknown event name.',\n NO_SUPPORT: 'Your web browser does not support the Gamepad API.'\n};\n\n\n\n\n//# sourceURL=webpack://gamecontroller.js/./src/constants.js?");
/***/ }),
/***/ "./src/gamecontrol.js":
/*!****************************!*\
!*** ./src/gamecontrol.js ***!
\****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _tools__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./tools */ \"./src/tools.js\");\n/* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./constants */ \"./src/constants.js\");\n/* harmony import */ var _gamepad__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./gamepad */ \"./src/gamepad.js\");\n\n\n\n\nconst gameControl = {\n gamepads: {},\n axeThreshold: [1.0], // this is an array so it can be expanded without breaking in the future\n isReady: (0,_tools__WEBPACK_IMPORTED_MODULE_0__.isGamepadSupported)(),\n onConnect: function() {},\n onDisconnect: function() {},\n onBeforeCycle: function() {},\n onAfterCycle: function() {},\n getGamepads: function() {\n return this.gamepads;\n },\n getGamepad: function(id) {\n if (this.gamepads[id]) {\n return this.gamepads[id];\n }\n return null;\n },\n set: function(property, value) {\n const properties = ['axeThreshold'];\n if (properties.indexOf(property) >= 0) {\n if (property === 'axeThreshold' && (!parseFloat(value) || value < 0.0 || value > 1.0)) {\n (0,_tools__WEBPACK_IMPORTED_MODULE_0__.error)(_constants__WEBPACK_IMPORTED_MODULE_1__.MESSAGES.INVALID_VALUE_NUMBER);\n return;\n }\n\n this[property] = value;\n\n if (property === 'axeThreshold') {\n const gps = this.getGamepads();\n const ids = Object.keys(gps);\n for (let x = 0; x < ids.length; x++) {\n gps[ids[x]].set('axeThreshold', this.axeThreshold);\n }\n }\n } else {\n (0,_tools__WEBPACK_IMPORTED_MODULE_0__.error)(_constants__WEBPACK_IMPORTED_MODULE_1__.MESSAGES.INVALID_PROPERTY);\n }\n },\n checkStatus: function() {\n const requestAnimationFrame =\n window.requestAnimationFrame || window.webkitRequestAnimationFrame;\n const gamepadIds = Object.keys(gameControl.gamepads);\n\n gameControl.onBeforeCycle();\n\n for (let x = 0; x < gamepadIds.length; x++) {\n gameControl.gamepads[gamepadIds[x]].checkStatus();\n }\n\n gameControl.onAfterCycle();\n\n if (gamepadIds.length > 0) {\n requestAnimationFrame(gameControl.checkStatus);\n }\n },\n init: function() {\n window.addEventListener('gamepadconnected', e => {\n const egp = e.gamepad || e.detail.gamepad;\n (0,_tools__WEBPACK_IMPORTED_MODULE_0__.log)(_constants__WEBPACK_IMPORTED_MODULE_1__.MESSAGES.ON);\n if (!window.gamepads) window.gamepads = {};\n if (egp) {\n if (!window.gamepads[egp.index]) {\n window.gamepads[egp.index] = egp;\n const gp = _gamepad__WEBPACK_IMPORTED_MODULE_2__.default.init(egp);\n gp.set('axeThreshold', this.axeThreshold);\n this.gamepads[gp.id] = gp;\n this.onConnect(this.gamepads[gp.id]);\n }\n if (Object.keys(this.gamepads).length === 1) this.checkStatus();\n }\n });\n window.addEventListener('gamepaddisconnected', e => {\n const egp = e.gamepad || e.detail.gamepad;\n (0,_tools__WEBPACK_IMPORTED_MODULE_0__.log)(_constants__WEBPACK_IMPORTED_MODULE_1__.MESSAGES.OFF);\n if (egp) {\n delete window.gamepads[egp.index];\n delete this.gamepads[egp.index];\n this.onDisconnect(egp.index);\n }\n });\n },\n on: function(eventName, callback) {\n switch (eventName) {\n case 'connect':\n this.onConnect = callback;\n break;\n case 'disconnect':\n this.onDisconnect = callback;\n break;\n case 'beforeCycle':\n case 'beforecycle':\n this.onBeforeCycle = callback;\n break;\n case 'afterCycle':\n case 'aftercycle':\n this.onAfterCycle = callback;\n break;\n default:\n (0,_tools__WEBPACK_IMPORTED_MODULE_0__.error)(_constants__WEBPACK_IMPORTED_MODULE_1__.MESSAGES.UNKNOWN_EVENT);\n break;\n }\n return this;\n },\n off: function(eventName) {\n switch (eventName) {\n case 'connect':\n this.onConnect = function() {};\n break;\n case 'disconnect':\n this.onDisconnect = function() {};\n break;\n case 'beforeCycle':\n case 'beforecycle':\n this.onBeforeCycle = function() {};\n break;\n case 'afterCycle':\n case 'aftercycle':\n this.onAfterCycle = function() {};\n break;\n default:\n (0,_tools__WEBPACK_IMPORTED_MODULE_0__.error)(_constants__WEBPACK_IMPORTED_MODULE_1__.MESSAGES.UNKNOWN_EVENT);\n break;\n }\n return this;\n }\n};\n\ngameControl.init();\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (gameControl);\n\n\n//# sourceURL=webpack://gamecontroller.js/./src/gamecontrol.js?");
/***/ }),
/***/ "./src/gamepad.js":
/*!************************!*\
!*** ./src/gamepad.js ***!
\************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _tools__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./tools */ \"./src/tools.js\");\n/* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./constants */ \"./src/constants.js\");\n\n\n\nconst gamepad = {\n init: function(gpad) {\n let gamepadPrototype = {\n id: gpad.index,\n buttons: gpad.buttons.length,\n axes: Math.floor(gpad.axes.length / 2),\n axeValues: [],\n axeThreshold: [1.0],\n hapticActuator: null,\n vibrationMode: -1,\n vibration: false,\n mapping: gpad.mapping,\n buttonActions: {},\n axesActions: {},\n pressed: {},\n set: function(property, value) {\n const properties = ['axeThreshold'];\n if (properties.indexOf(property) >= 0) {\n if (property === 'axeThreshold' && (!parseFloat(value) || value < 0.0 || value > 1.0)) {\n (0,_tools__WEBPACK_IMPORTED_MODULE_0__.error)(_constants__WEBPACK_IMPORTED_MODULE_1__.MESSAGES.INVALID_VALUE_NUMBER);\n return;\n }\n this[property] = value;\n } else {\n (0,_tools__WEBPACK_IMPORTED_MODULE_0__.error)(_constants__WEBPACK_IMPORTED_MODULE_1__.MESSAGES.INVALID_PROPERTY);\n }\n },\n vibrate: function(value = 0.75, duration = 500) {\n if (this.hapticActuator) {\n switch (this.vibrationMode) {\n case 0:\n return this.hapticActuator.pulse(value, duration);\n case 1:\n return this.hapticActuator.playEffect('dual-rumble', {\n duration: duration,\n strongMagnitude: value,\n weakMagnitude: value\n });\n }\n }\n },\n triggerDirectionalAction: function(id, axe, condition, x, index) {\n if (condition && x % 2 === index) {\n if (!this.pressed[`${id}${axe}`]) {\n this.pressed[`${id}${axe}`] = true;\n this.axesActions[axe][id].before();\n }\n this.axesActions[axe][id].action();\n } else if (this.pressed[`${id}${axe}`] && x % 2 === index) {\n delete this.pressed[`${id}${axe}`];\n this.axesActions[axe][id].after();\n }\n },\n checkStatus: function() {\n let gp = {};\n const gps = navigator.getGamepads\n ? navigator.getGamepads()\n : navigator.webkitGetGamepads\n ? navigator.webkitGetGamepads()\n : [];\n\n if (gps.length) {\n gp = gps[this.id];\n if (gp.buttons) {\n for (let x = 0; x < this.buttons; x++) {\n if (gp.buttons[x].pressed === true) {\n if (!this.pressed[`button${x}`]) {\n this.pressed[`button${x}`] = true;\n this.buttonActions[x].before();\n }\n this.buttonActions[x].action();\n } else if (this.pressed[`button${x}`]) {\n delete this.pressed[`button${x}`];\n this.buttonActions[x].after();\n }\n }\n }\n if (gp.axes) {\n const modifier = gp.axes.length % 2; // Firefox hack: detects one additional axe\n for (let x = 0; x < this.axes * 2; x++) {\n const val = gp.axes[x + modifier].toFixed(4);\n const axe = Math.floor(x / 2);\n this.axeValues[axe][x % 2] = val;\n\n this.triggerDirectionalAction('right', axe, val >= this.axeThreshold[0], x, 0);\n this.triggerDirectionalAction('left', axe, val <= -this.axeThreshold[0], x, 0);\n this.triggerDirectionalAction('down', axe, val >= this.axeThreshold[0], x, 1);\n this.triggerDirectionalAction('up', axe, val <= -this.axeThreshold[0], x, 1);\n }\n }\n }\n },\n associateEvent: function(eventName, callback, type) {\n if (eventName.match(/^button\\d+$/)) {\n const buttonId = parseInt(eventName.match(/^button(\\d+)$/)[1]);\n if (buttonId >= 0 && buttonId < this.buttons) {\n this.buttonActions[buttonId][type] = callback;\n } else {\n (0,_tools__WEBPACK_IMPORTED_MODULE_0__.error)(_constants__WEBPACK_IMPORTED_MODULE_1__.MESSAGES.INVALID_BUTTON);\n }\n } else if (eventName === 'start') {\n this.buttonActions[9][type] = callback;\n } else if (eventName === 'select') {\n this.buttonActions[8][type] = callback;\n } else if (eventName === 'r1') {\n this.buttonActions[5][type] = callback;\n } else if (eventName === 'r2') {\n this.buttonActions[7][type] = callback;\n } else if (eventName === 'l1') {\n this.buttonActions[4][type] = callback;\n } else if (eventName === 'l2') {\n this.buttonActions[6][type] = callback;\n } else if (eventName === 'power') {\n if (this.buttons >= 17) {\n this.buttonActions[16][type] = callback;\n } else {\n (0,_tools__WEBPACK_IMPORTED_MODULE_0__.error)(_constants__WEBPACK_IMPORTED_MODULE_1__.MESSAGES.INVALID_BUTTON);\n }\n } else if (eventName.match(/^(up|down|left|right)(\\d+)$/)) {\n const matches = eventName.match(/^(up|down|left|right)(\\d+)$/);\n const direction = matches[1];\n const axe = parseInt(matches[2]);\n if (axe >= 0 && axe < this.axes) {\n this.axesActions[axe][direction][type] = callback;\n } else {\n (0,_tools__WEBPACK_IMPORTED_MODULE_0__.error)(_constants__WEBPACK_IMPORTED_MODULE_1__.MESSAGES.INVALID_BUTTON);\n }\n } else if (eventName.match(/^(up|down|left|right)$/)) {\n const direction = eventName.match(/^(up|down|left|right)$/)[1];\n this.axesActions[0][direction][type] = callback;\n }\n return this;\n },\n on: function(eventName, callback) {\n return this.associateEvent(eventName, callback, 'action');\n },\n off: function(eventName) {\n return this.associateEvent(eventName, function() {}, 'action');\n },\n after: function(eventName, callback) {\n return this.associateEvent(eventName, callback, 'after');\n },\n before: function(eventName, callback) {\n return this.associateEvent(eventName, callback, 'before');\n }\n };\n\n for (let x = 0; x < gamepadPrototype.buttons; x++) {\n gamepadPrototype.buttonActions[x] = (0,_tools__WEBPACK_IMPORTED_MODULE_0__.emptyEvents)();\n }\n for (let x = 0; x < gamepadPrototype.axes; x++) {\n gamepadPrototype.axesActions[x] = {\n down: (0,_tools__WEBPACK_IMPORTED_MODULE_0__.emptyEvents)(),\n left: (0,_tools__WEBPACK_IMPORTED_MODULE_0__.emptyEvents)(),\n right: (0,_tools__WEBPACK_IMPORTED_MODULE_0__.emptyEvents)(),\n up: (0,_tools__WEBPACK_IMPORTED_MODULE_0__.emptyEvents)()\n };\n gamepadPrototype.axeValues[x] = [0, 0];\n }\n\n // check if vibration actuator exists\n if (gpad.hapticActuators) {\n // newer standard\n if (typeof gpad.hapticActuators.pulse === 'function') {\n gamepadPrototype.hapticActuator = gpad.hapticActuators;\n gamepadPrototype.vibrationMode = 0;\n gamepadPrototype.vibration = true;\n } else if (gpad.hapticActuators[0] && typeof gpad.hapticActuators[0].pulse === 'function') {\n gamepadPrototype.hapticActuator = gpad.hapticActuators[0];\n gamepadPrototype.vibrationMode = 0;\n gamepadPrototype.vibration = true;\n }\n } else if (gpad.vibrationActuator) {\n // old chrome stuff\n if (typeof gpad.vibrationActuator.playEffect === 'function') {\n gamepadPrototype.hapticActuator = gpad.vibrationActuator;\n gamepadPrototype.vibrationMode = 1;\n gamepadPrototype.vibration = true;\n }\n }\n\n return gamepadPrototype;\n }\n};\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (gamepad);\n\n\n//# sourceURL=webpack://gamecontroller.js/./src/gamepad.js?");
/***/ }),
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _tools__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./tools */ \"./src/tools.js\");\n/* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./constants */ \"./src/constants.js\");\n/* harmony import */ var _gamecontrol__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./gamecontrol */ \"./src/gamecontrol.js\");\n// This file is the entry point\n\n\n\n\nif ((0,_tools__WEBPACK_IMPORTED_MODULE_0__.isGamepadSupported)()) {\n window.gameControl = _gamecontrol__WEBPACK_IMPORTED_MODULE_2__.default;\n} else {\n (0,_tools__WEBPACK_IMPORTED_MODULE_0__.error)(_constants__WEBPACK_IMPORTED_MODULE_1__.MESSAGES.NO_SUPPORT);\n}\n\n\n//# sourceURL=webpack://gamecontroller.js/./src/index.js?");
/***/ }),
/***/ "./src/tools.js":
/*!**********************!*\
!*** ./src/tools.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"isGamepadSupported\": () => (/* binding */ isGamepadSupported),\n/* harmony export */ \"log\": () => (/* binding */ log),\n/* harmony export */ \"error\": () => (/* binding */ error),\n/* harmony export */ \"emptyEvents\": () => (/* binding */ emptyEvents)\n/* harmony export */ });\nconst log = (message, type = 'log') => {\n if (type === 'error') {\n if (console && typeof console.error === 'function') console.error(message);\n } else {\n if (console && typeof console.info === 'function') console.info(message);\n }\n};\n\nconst error = message => log(message, 'error');\n\nconst isGamepadSupported = () =>\n (navigator.getGamepads && typeof navigator.getGamepads === 'function') ||\n (navigator.getGamepads && typeof navigator.webkitGetGamepads === 'function') ||\n false;\n\nconst emptyEvents = () => ({ action: () => {}, after: () => {}, before: () => {} });\n\n\n\n\n//# sourceURL=webpack://gamecontroller.js/./src/tools.js?");
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = __webpack_require__("./src/index.js");
/******/
/******/ })()
;
================================================
FILE: examples/example-0-connectivity.html
================================================
GameControl Example: Connectivity
Example: Connectivity
Plug and unplug your gamepad(s) and see the changes below:
In some browsers, you may need to press a button after plugging in the gamepad.
How it works
There are two event associated to gameControl that you can use:
connect: triggered when a gamepad is connected to the browser.
disconnect: triggered when a gamepad is disconnected from the browser.
And an event handler can be easily associated to any of them using the
.on() method:
gameControl.on(EVENT_NAME, CALLBACK);
As an example, here is the code that manages the interaction above:
gameControl.on('connect', function() {
document.querySelector('#round-button').className = 'connected';
document.querySelector('#status').textContent = 'Device connected!';
});
gameControl.on('disconnect', function() {
document.querySelector('#round-button').className = 'disconnected';
document.querySelector('#status').textContent = 'Device disconnected!';
});
================================================
FILE: examples/example-3-buttons-and-joysticks.html
================================================
GameControl Example: Buttons and Joysticks
Example: Buttons and Joysticks
Connect a gamepad to the computer, and click the buttons or move the different
joysticks/axes. They will highlight as they are pressed.
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SELECT
START
This is a 17-button
standard gamepad layout as defined on the W3C Gamepad API definition . It may not match the gamepad that is connected , but it can be used to
see how the buttons/joysticks in that gamepad match the buttons in the standard gamepad.
How it works
Event handlers can be associated to the buttons using the .on() method:
gamepad.on(DIRECTION_NAME+AXE_ID, CALLBACK);
Only one action is allowed by button/joystick/axe. If you use several
.on() with the same direction, the latest call to it will be the one that is
applied when the joystick is pressed.
gameControl.on('connect', function(gamepad) {
gamepad.on('select', function() {
// do something
});
});
Some buttons/directions have aliases, so it is easier to associate events to them. For
example, if we only use the name of the direction ("up", "dowm", "right", or "left"), the
event handler will be associated to that direction in the first joystick/axe:
gamepad.on('up0', function() {
// do something
});
gamepad.on('up', function() {
// do something
});
Using aliases doesn't mean that you will be able to use more than one action per direction,
as they are the same.
The available direction aliases are:
up: equivalent to up0.
down: equivalent to down0.
right: equivalent to right0.
left: equivalent to left0.
The available button aliases are:
l1: equivalent to button4. Left back button 1
l2: equivalent to button6. Left back button 2
r1: equivalent to button5. Right back button 1
r2: equivalent to button7. Right back button 2
select: equivalent to button8. (Works for Select /Back
in some gamepads)
start: equivalent to button9. (Works for Start /Forward
in some gamepads)
power: equivalent to button16. It is the power button available
on some gamepads (e.g. the XBox logo button)
================================================
FILE: examples/example-4-snes-controller.html
================================================
GameControl Example: SNES controller
Connect your gamepad and press any button...
X
A
Y
B
START
SELECT
================================================
FILE: examples/example-5-alvanoid.html
================================================
GameControl Example: Alvanoid
================================================
FILE: examples/example-6-multiplayer.html
================================================
GameControl Example: Multiplayer (Pong)
================================================
FILE: examples/example-7-joystick-threshold.html
================================================
GameControl Example: Joystick Sensitivity Threshold
Example: Joystick Sensitivity Threshold
Adjust the level of sensitivity for the axe/joystick, then move the primary axe (see
diagram of layout ), and see when when
the action would trigger (the circle will turn green).
How it works
Changing the axe/joystick sensitivity with gameController.js is really simple. Use
.set(PROPERTY, VALUE) to change the value of "axeThreshold"
The passed value can range from 0 to 1, and it can be set at the gamepad level
or at the gameControl level (and all the gamepads will be updated with that
value.) Here is an example of how it can be done:
gameControl.on('connect', function(gamepad) {
// all the existing gamepads will have a threshold of 0.75
this.set('axeThreshold', 0.75);
// the newly detected gamepad will have a threshold of 0.5
gamepad.set('axeThreshold', 0.5);
});
================================================
FILE: examples/example-8-vibration.html
================================================
GameControl Example: Vibration
Example: Vibration
This is an experimental feature. Not all gamepad/joysticks will support vibration on a
browser.
Even the ones that can vibrate, may not be able to vibrate on a browser because this is a
not widely supported feature.
Connect your gamepad/joystick, and click on the button to trigger the vibration.
No gamepad/joystick connected
How it works
Vibration of the gamepad can be triggered using the .vibrate() method of the
gamepad object. That method takes two parameters:
Intensity: the strength of the vibration. It is value between 0 and 1.
Duration: the duration in milliseconds of the vibration.
The .vibrate() method can be called without parameters. In that case, the
intensity will be 0.75 by default, and the duration will be 500 milliseconds (half a
second.)
gameControl.on('connect', function(gamepad) {
if (gamepad.vibration) {
gamepad.vibrate(0.5, 1000);
}
});
If several vibrations are called, they won't be chained. Instead the last one will overwrite
the existing one.
================================================
FILE: examples/example-9-before-and-after.html
================================================
GameControl Example: Before and After Events
Example: Before and After Events
Connect a gamepad to the computer, then press the Start and Select buttons and see the
numbers update below.
Select pressed: 0 times.
Start pressed: 0 times.
This is achieved using the .before() and .after() event handlers.
And it can be really convenient in many situations: setting up timers, mimic a single press
instead of a continuous one, etc.
How it works
The .before() and .after() methods work in a similar way to how
.on() does:
gamepad.before(EVENTNAME, CALLBACK);
gamepad.on(EVENTNAME, CALLBACK);
gamepad.after(EVENTNAME, CALLBACK);
These methods can be chained to allow a more fluid development. So the example above could
also be written as:
gamepad.before(EVENTNAME, CALLBACK);
.on(EVENTNAME, CALLBACK);
.after(EVENTNAME, CALLBACK);
================================================
FILE: examples/examples.css
================================================
html,
body {
background: #666;
border: 0;
display: flex;
flex-direction: column;
font-family: Arial, Verdana, sans-serif;
font-size: 16px;
margin: 0;
min-height: 100vh;
min-width: 100vw;
padding: 0;
}
main {
background: #eee;
box-sizing: border-box;
border: 1rem solid #333;
border-radius: 0.25rem;
color: #222;
flex: 1;
margin: 3rem auto;
max-width: 1000px;
min-width: 320px;
padding: 2rem;
width: 100%;
width: calc(100% - 6rem);
}
h1 {
font-size: 2rem;
margin: 0 auto 1rem auto;
}
h2 {
font-size: 1.75rem;
margin: 2rem auto 1rem auto;
}
p,
ul,
pre.code {
font-size: 1.1rem;
margin: 0 0 1rem 0;
line-height: 1.5rem;
}
p.note {
background: rgba(0, 0, 0, 0.075);
border-left: 0.25rem solid #222;
padding: 0.5rem;
}
p.note::before {
content: 'Note: ';
font-weight: bold;
}
p.code {
margin-left: 2rem;
}
pre.code {
background: #333;
color: #ddd;
overflow-x: auto;
padding: 1rem;
}
================================================
FILE: license.md
================================================
Copyright (c) 2019 - Alvaro Montoro (alvaromontoro@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: package.json
================================================
{
"name": "gamecontroller.js",
"version": "1.5.0",
"description": "A JavaScript library that lets you handle, configure, and use gamepad and controllers on a browser, using the Gamepad API",
"main": "dist/gamecontroller.min.js",
"scripts": {
"build": "webpack --output-filename gamecontroller.min.js --mode production",
"build:dev": "webpack --output-filename gamecontroller.js --mode development",
"test": "jest --coverage"
},
"devDependencies": {
"@babel/preset-env": "^7.13.8",
"jest": "^26.6.3",
"webpack": "^5.4.0",
"webpack-cli": "^4.5.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/alvaromontoro/gamecontrol.git"
},
"bugs": {
"url": "https://github.com/alvaromontoro/gamecontrol/issues"
},
"homepage": "https://github.com/alvaromontoro/gamecontrol",
"author": "Alvaro Montoro",
"license": "MIT",
"keywords": [
"game control",
"gamepad",
"game controller",
"game control",
"controller",
"gaming",
"web api",
"html5 api",
"gamepad api"
],
"directories": {
"example": "examples",
"test": "tests"
}
}
================================================
FILE: readme.md
================================================
# gameController.js
A JavaScript library that lets you handle, configure, and use gamepad and controllers on a browser.
[](https://travis-ci.org/alvaromontoro/gamecontroller.js)
[](https://www.npmjs.com/package/gamecontroller.js)
[](https://www.npmjs.com/package/gamecontroller.js)
## Getting started
GameController.js is a lightweight library (~6KB) that uses JavaScript and the standard [Gamepad API](https://w3c.github.io/gamepad/), and does not have any plugin/library dependencies.
## Installation
From npm:
```
npm i gamecontroller.js
```
From yarn:
```
yarn add gamecontroller.js
```
Directly into your webpage (check [latest release on github](https://github.com/alvaromontoro/gamecontroller.js/releases)):
```
```
## Usage
After importing the library into your webpage/project, `gameControl` will be available to use. This object comes with a series of properties and methods that will allow to handle the different gamepads connected to the computer.
The connected gamepads will be stored in a list of `gamepad` objects in `gameControl`. **This `gamepad` object is not the default one returned by the browser** but a higher-level interface to interact with it and simplify its usability.
Once the file is imported into the project, the object `gameControl` will be available and ready to be used.
```javascript
gameControl.on('connect', function(gamepad) {
gamepad.on('up', moveCharacterUp);
});
```
[Visit the Wiki for a full list of the properties, methods and events](https://github.com/alvaromontoro/gamecontroller.js/wiki) of these two objects.
### Events for gameControl
For the object `gameControl`, events are associated using the `.on()` method:
```javascript
gameControl.on('connect', gamepad => {
console.log('A new gamepad was connected!');
});
```
Here is a list of the events that can be associated using `.on()`:
Name
Description
connect
Triggered every time that a gamepad is connected to the browser. It returns an instance of the `gamepad` object described below.
disconnect
Triggered when a gamepad is disconnected from the browser.
beforeCycle
Triggered before the gamepads are checked for pressed buttons/joysticks movement (before those events are triggered).
afterCycle
Triggered after the gamepads are checked for pressed buttons/joysticks movement (after those events have been triggered).
### Events for gamepad
The events for the `gamepad` objects work a little bit different. The event name, is the name of the button/direction that was activated (e.g. `button0`, `up`, etc.) And there are three functions that can be used to associate event handlers for them in different situations:
- `.on()`: triggered every cycle, while the button/joystick is pressed/active.
- `.before()`: triggered the first cycle that a button/joystick is pressed.
- `.after()`: triggered the first cycle after a button/joystick stopped being pressed.
All three functions can be chained and allow two parameters: the first one is the button/direction that was activated, and the second parameter is the callback function. Example:
```javascript
gamepad.on('button0', () => { console.log('Button 0 still pressed...'); })
.before('button0', () => { console.log('Button 0 pressed...'); })
.after('button0', () => { console.log('Button 0 was released'; });
```
To see the event flow and how the different events are lined-up and interact with each other, visit the [Event Flow wikipage](../EventFlow).
Thisus
These are the _events_ that can be passed as first parameter to the event functions:
Name
Description
button0
Triggered when button 0 is pressed.
button1
Triggered when button 1 is pressed.
button2
Triggered when button 2 is pressed.
button3
Triggered when button 3 is pressed.
button4
Triggered when button 4 is pressed.
button5
Triggered when button 5 is pressed.
button6
Triggered when button 6 is pressed.
button7
Triggered when button 7 is pressed.
button8
Triggered when button 8 is pressed.
button9
Triggered when button 9 is pressed.
button10
Triggered when button 10 is pressed.
button11
Triggered when button 11 is pressed.
button12
Triggered when button 12 is pressed.
button13
Triggered when button 13 is pressed.
button14
Triggered when button 14 is pressed.
button15
Triggered when button 15 is pressed.
button16
Triggered when button 16 is pressed.
up0
Triggered when the first axe/joystick is moved up.
down0
Triggered when the first axe/joystick is moved down.
right0
Triggered when the first axe/joystick is moved right.
left0
Triggered when the first axe/joystick is moved left.
up1
Triggered when the second axe/joystick is moved up.
down1
Triggered when the second axe/joystick is moved down.
right1
Triggered when the second axe/joystick is moved right.
left1
Triggered when the second axe/joystick is moved left.
start
Triggered when Start button is pressed. This is an alias for event button9.
select
Triggered when Select button is pressed. This is an alias for event button8.
power
Triggered when Power button is pressed (e.g. the Xbox logo in an Xbox controller). This is an alias for event button16.
l1
Triggered when the left back button 1 is pressed. This is an alias for event button4.
l2
Triggered when left back button 2 is pressed. This is an alias for event button6.
r1
Triggered when right back button 1 is pressed. This is an alias for event button5.
r2
Triggered when right back button 2 is pressed. This is an alias for event button7.
up
Triggered when the main/first axe/joystick is moved up. This is an alias for event up0.
down
Triggered when the main/first axe/joystick is moved down. This is an alias for event down0.
right
Triggered when the main/first axe/joystick is moved right. This is an alias for event right0.
left
Triggered when the main/first axe/joystick is moved left. This is an alias for event left0.
These names are not arbitrary. They match the buttons and axes described in the [W3C Gamepad API specicification](https://w3c.github.io/gamepad/#fig-visual-representation-of-a-standard-gamepad-layout):

## Browser Support
|  Edge |  Firefox |  Chrome |  Safari |  Opera |
| ---- | ------- | ------ | ------ | ----- |
| 12+ | 29+ | 25+ | 10.1+ | 24+ |
## Examples
The `examples` folder contains different examples to showcase how to use the library:
- [Connectivity](https://htmlpreview.github.io/?https://github.com/alvaromontoro/gamecontroller.js/blob/master/examples/example-0-connectivity.html): shows how to detect if a gamepad was connected/disconnected.
- [Buttons and Joysticks](https://htmlpreview.github.io/?https://github.com/alvaromontoro/gamecontroller.js/blob/master/examples/example-3-buttons-and-joysticks.html): see how the buttons from your gamepad map to the default gamepad.
- [SNES Controller](https://htmlpreview.github.io/?https://github.com/alvaromontoro/gamecontroller.js/blob/master/examples/example-4-snes-controller.html): replica of a SNES controller (based on a previous CodePen demo).
- [Alvanoid](https://htmlpreview.github.io/?https://github.com/alvaromontoro/gamecontroller.js/blob/master/examples/example-5-alvanoid.html): small Arkanoid-based game (based on a previous CodePen demo).
- [Pong](https://htmlpreview.github.io/?https://github.com/alvaromontoro/gamecontroller.js/blob/master/examples/example-6-multiplayer.html): multiplayer demo with the classic game Pong for 2 players on 2 gamepads.
================================================
FILE: src/constants.js
================================================
const MESSAGES = {
ON: 'Gamepad detected.',
OFF: 'Gamepad disconnected.',
INVALID_PROPERTY: 'Invalid property.',
INVALID_VALUE_NUMBER: 'Invalid value. It must be a number between 0.00 and 1.00.',
INVALID_BUTTON: 'Button does not exist.',
UNKNOWN_EVENT: 'Unknown event name.',
NO_SUPPORT: 'Your web browser does not support the Gamepad API.'
};
export { MESSAGES };
================================================
FILE: src/gamecontrol.js
================================================
import { log, error, isGamepadSupported } from './tools';
import { MESSAGES } from './constants';
import gamepad from './gamepad';
const gameControl = {
gamepads: {},
axeThreshold: [1.0], // this is an array so it can be expanded without breaking in the future
isReady: isGamepadSupported(),
onConnect: function() {},
onDisconnect: function() {},
onBeforeCycle: function() {},
onAfterCycle: function() {},
getGamepads: function() {
return this.gamepads;
},
getGamepad: function(id) {
if (this.gamepads[id]) {
return this.gamepads[id];
}
return null;
},
set: function(property, value) {
const properties = ['axeThreshold'];
if (properties.indexOf(property) >= 0) {
if (property === 'axeThreshold' && (!parseFloat(value) || value < 0.0 || value > 1.0)) {
error(MESSAGES.INVALID_VALUE_NUMBER);
return;
}
this[property] = value;
if (property === 'axeThreshold') {
const gps = this.getGamepads();
const ids = Object.keys(gps);
for (let x = 0; x < ids.length; x++) {
gps[ids[x]].set('axeThreshold', this.axeThreshold);
}
}
} else {
error(MESSAGES.INVALID_PROPERTY);
}
},
checkStatus: function() {
const requestAnimationFrame =
window.requestAnimationFrame || window.webkitRequestAnimationFrame;
const gamepadIds = Object.keys(gameControl.gamepads);
gameControl.onBeforeCycle();
for (let x = 0; x < gamepadIds.length; x++) {
gameControl.gamepads[gamepadIds[x]].checkStatus();
}
gameControl.onAfterCycle();
if (gamepadIds.length > 0) {
requestAnimationFrame(gameControl.checkStatus);
}
},
init: function() {
window.addEventListener('gamepadconnected', e => {
const egp = e.gamepad || e.detail.gamepad;
log(MESSAGES.ON);
if (!window.gamepads) window.gamepads = {};
if (egp) {
if (!window.gamepads[egp.index]) {
window.gamepads[egp.index] = egp;
const gp = gamepad.init(egp);
gp.set('axeThreshold', this.axeThreshold);
this.gamepads[gp.id] = gp;
this.onConnect(this.gamepads[gp.id]);
}
if (Object.keys(this.gamepads).length === 1) this.checkStatus();
}
});
window.addEventListener('gamepaddisconnected', e => {
const egp = e.gamepad || e.detail.gamepad;
log(MESSAGES.OFF);
if (egp) {
delete window.gamepads[egp.index];
delete this.gamepads[egp.index];
this.onDisconnect(egp.index);
}
});
},
on: function(eventName, callback) {
switch (eventName) {
case 'connect':
this.onConnect = callback;
break;
case 'disconnect':
this.onDisconnect = callback;
break;
case 'beforeCycle':
case 'beforecycle':
this.onBeforeCycle = callback;
break;
case 'afterCycle':
case 'aftercycle':
this.onAfterCycle = callback;
break;
default:
error(MESSAGES.UNKNOWN_EVENT);
break;
}
return this;
},
off: function(eventName) {
switch (eventName) {
case 'connect':
this.onConnect = function() {};
break;
case 'disconnect':
this.onDisconnect = function() {};
break;
case 'beforeCycle':
case 'beforecycle':
this.onBeforeCycle = function() {};
break;
case 'afterCycle':
case 'aftercycle':
this.onAfterCycle = function() {};
break;
default:
error(MESSAGES.UNKNOWN_EVENT);
break;
}
return this;
}
};
gameControl.init();
export default gameControl;
================================================
FILE: src/gamepad.js
================================================
import { error, emptyEvents } from './tools';
import { MESSAGES } from './constants';
const gamepad = {
init: function(gpad) {
let gamepadPrototype = {
id: gpad.index,
buttons: gpad.buttons.length,
axes: Math.floor(gpad.axes.length / 2),
axeValues: [],
axeThreshold: [1.0],
hapticActuator: null,
vibrationMode: -1,
vibration: false,
mapping: gpad.mapping,
buttonActions: {},
axesActions: {},
pressed: {},
set: function(property, value) {
const properties = ['axeThreshold'];
if (properties.indexOf(property) >= 0) {
if (property === 'axeThreshold' && (!parseFloat(value) || value < 0.0 || value > 1.0)) {
error(MESSAGES.INVALID_VALUE_NUMBER);
return;
}
this[property] = value;
} else {
error(MESSAGES.INVALID_PROPERTY);
}
},
vibrate: function(value = 0.75, duration = 500) {
if (this.hapticActuator) {
switch (this.vibrationMode) {
case 0:
return this.hapticActuator.pulse(value, duration);
case 1:
return this.hapticActuator.playEffect('dual-rumble', {
duration: duration,
strongMagnitude: value,
weakMagnitude: value
});
}
}
},
triggerDirectionalAction: function(id, axe, condition, x, index) {
if (condition && x % 2 === index) {
if (!this.pressed[`${id}${axe}`]) {
this.pressed[`${id}${axe}`] = true;
this.axesActions[axe][id].before();
}
this.axesActions[axe][id].action();
} else if (this.pressed[`${id}${axe}`] && x % 2 === index) {
delete this.pressed[`${id}${axe}`];
this.axesActions[axe][id].after();
}
},
checkStatus: function() {
let gp = {};
const gps = navigator.getGamepads
? navigator.getGamepads()
: navigator.webkitGetGamepads
? navigator.webkitGetGamepads()
: [];
if (gps.length) {
gp = gps[this.id];
if (gp.buttons) {
for (let x = 0; x < this.buttons; x++) {
if (gp.buttons[x].pressed === true) {
if (!this.pressed[`button${x}`]) {
this.pressed[`button${x}`] = true;
this.buttonActions[x].before();
}
this.buttonActions[x].action();
} else if (this.pressed[`button${x}`]) {
delete this.pressed[`button${x}`];
this.buttonActions[x].after();
}
}
}
if (gp.axes) {
const modifier = gp.axes.length % 2; // Firefox hack: detects one additional axe
for (let x = 0; x < this.axes * 2; x++) {
const val = gp.axes[x + modifier].toFixed(4);
const axe = Math.floor(x / 2);
this.axeValues[axe][x % 2] = val;
this.triggerDirectionalAction('right', axe, val >= this.axeThreshold[0], x, 0);
this.triggerDirectionalAction('left', axe, val <= -this.axeThreshold[0], x, 0);
this.triggerDirectionalAction('down', axe, val >= this.axeThreshold[0], x, 1);
this.triggerDirectionalAction('up', axe, val <= -this.axeThreshold[0], x, 1);
}
}
}
},
associateEvent: function(eventName, callback, type) {
if (eventName.match(/^button\d+$/)) {
const buttonId = parseInt(eventName.match(/^button(\d+)$/)[1]);
if (buttonId >= 0 && buttonId < this.buttons) {
this.buttonActions[buttonId][type] = callback;
} else {
error(MESSAGES.INVALID_BUTTON);
}
} else if (eventName === 'start') {
this.buttonActions[9][type] = callback;
} else if (eventName === 'select') {
this.buttonActions[8][type] = callback;
} else if (eventName === 'r1') {
this.buttonActions[5][type] = callback;
} else if (eventName === 'r2') {
this.buttonActions[7][type] = callback;
} else if (eventName === 'l1') {
this.buttonActions[4][type] = callback;
} else if (eventName === 'l2') {
this.buttonActions[6][type] = callback;
} else if (eventName === 'power') {
if (this.buttons >= 17) {
this.buttonActions[16][type] = callback;
} else {
error(MESSAGES.INVALID_BUTTON);
}
} else if (eventName.match(/^(up|down|left|right)(\d+)$/)) {
const matches = eventName.match(/^(up|down|left|right)(\d+)$/);
const direction = matches[1];
const axe = parseInt(matches[2]);
if (axe >= 0 && axe < this.axes) {
this.axesActions[axe][direction][type] = callback;
} else {
error(MESSAGES.INVALID_BUTTON);
}
} else if (eventName.match(/^(up|down|left|right)$/)) {
const direction = eventName.match(/^(up|down|left|right)$/)[1];
this.axesActions[0][direction][type] = callback;
}
return this;
},
on: function(eventName, callback) {
return this.associateEvent(eventName, callback, 'action');
},
off: function(eventName) {
return this.associateEvent(eventName, function() {}, 'action');
},
after: function(eventName, callback) {
return this.associateEvent(eventName, callback, 'after');
},
before: function(eventName, callback) {
return this.associateEvent(eventName, callback, 'before');
}
};
for (let x = 0; x < gamepadPrototype.buttons; x++) {
gamepadPrototype.buttonActions[x] = emptyEvents();
}
for (let x = 0; x < gamepadPrototype.axes; x++) {
gamepadPrototype.axesActions[x] = {
down: emptyEvents(),
left: emptyEvents(),
right: emptyEvents(),
up: emptyEvents()
};
gamepadPrototype.axeValues[x] = [0, 0];
}
// check if vibration actuator exists
if (gpad.hapticActuators) {
// newer standard
if (typeof gpad.hapticActuators.pulse === 'function') {
gamepadPrototype.hapticActuator = gpad.hapticActuators;
gamepadPrototype.vibrationMode = 0;
gamepadPrototype.vibration = true;
} else if (gpad.hapticActuators[0] && typeof gpad.hapticActuators[0].pulse === 'function') {
gamepadPrototype.hapticActuator = gpad.hapticActuators[0];
gamepadPrototype.vibrationMode = 0;
gamepadPrototype.vibration = true;
}
} else if (gpad.vibrationActuator) {
// old chrome stuff
if (typeof gpad.vibrationActuator.playEffect === 'function') {
gamepadPrototype.hapticActuator = gpad.vibrationActuator;
gamepadPrototype.vibrationMode = 1;
gamepadPrototype.vibration = true;
}
}
return gamepadPrototype;
}
};
export default gamepad;
================================================
FILE: src/index.js
================================================
// This file is the entry point
import { error, isGamepadSupported } from './tools';
import { MESSAGES } from './constants';
import gameControl from './gamecontrol';
if (isGamepadSupported()) {
window.gameControl = gameControl;
} else {
error(MESSAGES.NO_SUPPORT);
}
================================================
FILE: src/tools.js
================================================
const log = (message, type = 'log') => {
if (type === 'error') {
if (console && typeof console.error === 'function') console.error(message);
} else {
if (console && typeof console.info === 'function') console.info(message);
}
};
const error = message => log(message, 'error');
const isGamepadSupported = () =>
(navigator.getGamepads && typeof navigator.getGamepads === 'function') ||
(navigator.getGamepads && typeof navigator.webkitGetGamepads === 'function') ||
false;
const emptyEvents = () => ({ action: () => {}, after: () => {}, before: () => {} });
export { isGamepadSupported, log, error, emptyEvents };
================================================
FILE: tests/gamecontrol.test.js
================================================
import gameControl from '../src/gamecontrol';
import gamepad from '../src/gamepad';
import { gamepads } from './mock.gamepads';
function generateGamepads() {
const auxGamepads = {};
for (let x = 0; x < gamepads.length; x++) {
auxGamepads[x] = gamepad.init(gamepads[x]);
auxGamepads[x].set('axeThreshold', gameControl.axeThreshold);
}
gameControl.gamepads = auxGamepads;
}
describe('gameControl', () => {
// these cases should probably not happen but should fail gracefully
test('Check status when nothing has been connected yet', () => {
global.webkitRequestAnimationFrame = global.requestAnimationFrame;
global.requestAnimationFrame = null;
gameControl.checkStatus();
global.requestAnimationFrame = global.webkitRequestAnimationFrame;
});
test('Check gameControl gamepads', () => {
expect(gameControl.gamepads).toEqual({});
expect(gameControl.getGamepads()).toEqual({});
expect(gameControl.getGamepad(0)).toEqual(null);
});
test('trigger event gamepadconnected', () => {
const event = new CustomEvent('gamepadconnected', {
detail: { gamepad: gamepads[0] },
gamepad: gamepads[0]
});
global.dispatchEvent(event);
});
test('trigger event gamepaddisconnected', () => {
const event = new CustomEvent('gamepaddisconnected', {
detail: { gamepad: gamepads[0] }
});
global.dispatchEvent(event);
});
// this definitely should not happen
test('trigger event gamepadconnected (no gamepad)', () => {
const event = new CustomEvent('gamepadconnected', {
detail: {}
});
global.dispatchEvent(event);
});
// this should not happen
test('trigger event gamepaddisconnected (no gamepad)', () => {
const event = new CustomEvent('gamepaddisconnected', {
detail: {}
});
global.dispatchEvent(event);
});
test('trigger event gamepadconnected for three gamepads', () => {
const event = new CustomEvent('gamepadconnected', {
detail: { gamepad: gamepads[0] },
gamepad: gamepads[0]
});
global.dispatchEvent(event);
const event2 = new CustomEvent('gamepadconnected', {
detail: { gamepad: gamepads[1] },
gamepad: gamepads[1]
});
global.dispatchEvent(event2);
// this probably shouldn't happen
const event3 = new CustomEvent('gamepadconnected', {
detail: { gamepad: gamepads[0] },
gamepad: gamepads[0]
});
global.dispatchEvent(event3);
});
test('Function getGamepads()', () => {
generateGamepads();
const gamepadList = gameControl.getGamepads();
expect(Object.keys(gamepadList).length).toEqual(gamepads.length);
});
test('Function getGamepad(id)', () => {
generateGamepads();
const gp = gameControl.getGamepad(0);
expect(gp.id).toEqual(0);
expect(gp.mapping).toEqual('standard');
});
test('Function getGamepad(id) incorrect id', () => {
generateGamepads();
const gp = gameControl.getGamepad(100);
expect(gp).toEqual(null);
});
test('Verify sensitivity threshold', () => {
generateGamepads();
const gp = gameControl.getGamepad(0);
expect(gp.axeThreshold[0]).toEqual(1.0);
gp.set('axeThreshold', [0.3]);
expect(gp.axeThreshold[0]).toEqual(0.3);
});
test('Verify sensitivity threshold', () => {
gameControl.axeThreshold = [0.5];
generateGamepads();
const gp = gameControl.getGamepad(0);
expect(gp.axeThreshold[0]).toEqual(0.5);
gp.set('axeThreshold', [0.3]);
expect(gp.axeThreshold[0]).toEqual(0.3);
gameControl.axeThreshold = [0.5];
expect(gp.axeThreshold[0]).toEqual(0.3);
});
test('event associattion and deassociation', () => {
gameControl
.on('connect', () => 'gamepad connected')
.on('disconnect', () => 'gamepad disconnected')
.on('beforecycle', () => 'before cycle')
.on('aftercycle', () => 'after cycle')
.on('afterCycle', () => 'after Cycle')
.on('beforeCycle', () => 'before Cycle');
expect(gameControl.onConnect()).toEqual('gamepad connected');
gameControl
.off('connect')
.off('disconnect')
.off('beforecycle')
.off('aftercycle')
.off('afterCycle')
.off('beforeCycle');
expect(gameControl.onConnect()).toEqual(undefined);
});
test('event association/deassociation of unknown event', () => {
gameControl.on('invalidEvent', () => 'invalid event');
gameControl.off('invalidEvent');
});
test('checkStatus', () => {
generateGamepads();
gameControl.checkStatus();
});
test('set invalid property', () => {
gameControl.set('invalidProperty', true);
});
test('set axeThreshold', () => {
gameControl.set('axeThreshold', [1.0]);
expect(gameControl.axeThreshold[0]).toEqual(1.0);
gameControl.set('axeThreshold', [0.5]);
expect(gameControl.axeThreshold[0]).toEqual(0.5);
gameControl.set('axeThreshold', [10.5]);
expect(gameControl.axeThreshold[0]).toEqual(0.5);
});
});
================================================
FILE: tests/gamepad.test.js
================================================
import gamepad from '../src/gamepad';
import { gamepads, gamepadsFirefox } from './mock.gamepads';
describe('gamepad', () => {
test('Check default values (17-button gamepad)', () => {
const gp = gamepad.init(gamepads[0]);
expect(gp.id).toEqual(0);
expect(gp.buttons).toEqual(17);
expect(gp.axes).toEqual(2);
expect(gp.mapping).toEqual('standard');
expect(Object.keys(gp.buttonActions).length).toEqual(gp.buttons);
expect(Object.keys(gp.axesActions).length).toEqual(gp.axes);
});
test('Check default values (10-button gamepad)', () => {
const gp = gamepad.init(gamepads[1]);
expect(gp.id).toEqual(1);
expect(gp.buttons).toEqual(10);
expect(gp.axes).toEqual(1);
expect(gp.mapping).toEqual('');
expect(Object.keys(gp.buttonActions).length).toEqual(gp.buttons);
expect(Object.keys(gp.axesActions).length).toEqual(gp.axes);
});
test('Check button pressed (manual)', () => {
const gp = gamepad.init(gamepads[0]);
const message = 'Button0 pressed';
gp.on('button0', function() {
return message;
});
expect(gp.buttonActions[0].action()).toEqual(message);
});
test('Verify sensitivity threshold', () => {
const gp = gamepad.init(gamepads[0]);
expect(gp.axeThreshold[0]).toEqual(1.0);
gp.set('axeThreshold', [0.3]);
expect(gp.axeThreshold[0]).toEqual(0.3);
gp.set('axeThreshold', [12.0]); // invalid value
expect(gp.axeThreshold[0]).toEqual(0.3);
});
test('set invalid property', () => {
const gp = gamepad.init(gamepads[0]);
gp.set('invalidProperty', true);
});
test('Vibration in Chrome', () => {
const gp = gamepad.init(gamepads[0]);
expect(gp.vibrate(0.5, 500)).toEqual('dual-rumble - 500');
expect(gp.vibrate()).toEqual('dual-rumble - 500');
});
// this case should never happen
test('Vibration in Chrome (no vibration)', () => {
const gp = gamepad.init(gamepads[2]);
expect(gp.vibrate(0.5, 500)).toEqual(undefined);
});
test('Vibration in Firefox', () => {
const gp = gamepad.init(gamepadsFirefox[0]);
expect(gp.vibrate(0.5, 500)).toEqual('vibrate at 0.5 for 500ms');
});
test('Vibration in Firefox (no vibration)', () => {
const gp = gamepad.init(gamepadsFirefox[1]);
expect(gp.vibrate(0.5, 500)).toEqual(undefined);
});
// this case should never happen
test('Vibration in Firefox (wrong type)', () => {
const gp = gamepad.init(gamepadsFirefox[3]);
expect(gp.vibrate(0.5, 500)).toEqual(undefined);
});
test('Vibration in Firefox (array of actuators)', () => {
const gp = gamepad.init(gamepadsFirefox[2]);
expect(gp.vibrate(0.5, 500)).toEqual('vibrate at 0.5 for 500ms');
});
test('after event', () => {
const gp = gamepad.init(gamepads[0]);
gp.after('button0', function() {
return 'button0 released';
});
expect(gp.buttonActions[0].after()).toEqual('button0 released');
});
test('on event', () => {
const gp = gamepad.init(gamepads[0]);
gp.on('button0', function() {
return 'button0 is on';
});
expect(gp.buttonActions[0].action()).toEqual('button0 is on');
});
test('on wrong event', () => {
const gp = gamepad.init(gamepads[0]);
gp.on('fakeevent', () => {});
});
test('before event', () => {
const gp = gamepad.init(gamepads[0]);
gp.before('button0', function() {
return 'button0 pressed';
});
expect(gp.buttonActions[0].before()).toEqual('button0 pressed');
});
test('off event', () => {
const gp = gamepad.init(gamepads[0]);
gp.on('button0', function() {
return 'button0 is on';
});
expect(gp.buttonActions[0].action()).toEqual('button0 is on');
gp.off('button0');
expect(gp.buttonActions[0].action()).toEqual(undefined);
});
test('on directional event', () => {
const gp = gamepad.init(gamepads[0]);
gp.on('up0', function() {
return 'up0 is on';
});
expect(gp.axesActions[0].up.action()).toEqual('up0 is on');
gp.off('up0');
expect(gp.axesActions[0].up.action()).toEqual(undefined);
});
test('on directional event (alias)', () => {
const gp = gamepad.init(gamepads[0]);
gp.on('up', function() {
return 'up is on';
});
expect(gp.axesActions[0].up.action()).toEqual('up is on');
gp.off('up');
expect(gp.axesActions[0].up.action()).toEqual(undefined);
});
test('on directional event (incorrect)', () => {
const gp = gamepad.init(gamepads[0]);
gp.on('up4', function() {
return 'up4 is on';
});
});
test('on button aliases', () => {
const gp = gamepad.init(gamepads[0]);
gp.on('select', () => 'select')
.on('start', () => 'start')
.on('l1', () => 'l1')
.on('l2', () => 'l2')
.on('r1', () => 'r1')
.on('r2', () => 'r2')
.on('power', () => 'power');
expect(gp.buttonActions[8].action()).toEqual('select');
expect(gp.buttonActions[9].action()).toEqual('start');
expect(gp.buttonActions[4].action()).toEqual('l1');
expect(gp.buttonActions[5].action()).toEqual('r1');
expect(gp.buttonActions[6].action()).toEqual('l2');
expect(gp.buttonActions[7].action()).toEqual('r2');
expect(gp.buttonActions[16].action()).toEqual('power');
});
test('on button power when no button power', () => {
const gp = gamepad.init(gamepads[1]);
gp.on('power', () => 'power');
});
test('on button outside of range', () => {
const gp = gamepad.init(gamepads[1]);
gp.on('button1234', () => 'event on incorrect button');
});
test('cycle check status', () => {
const gp = gamepad.init(gamepads[1]);
const mockGamepads = () => gamepads;
global.navigator.getGamepads = mockGamepads;
gp.checkStatus();
gamepads[1].buttons[0].pressed = false;
gamepads[1].axes[0] = 0.0;
gp.checkStatus();
});
// this should not happen
test('cycle check status (no axes)', () => {
const gp = gamepad.init(gamepads[2]);
gamepads[2].axes = null;
gamepads[2].buttons = null;
const mockGamepads = () => gamepads;
global.navigator.getGamepads = mockGamepads;
gp.checkStatus();
global.navigator.getGamepads = null;
global.navigator.webkitGetGamepads = mockGamepads;
gp.checkStatus();
});
});
================================================
FILE: tests/mock.gamepads.js
================================================
const gamepads = [
{
axes: [0, 0, 0, 0],
buttons: [
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 }
],
connected: true,
id: '17-button gamepad mockup (STANDARD Vendor: Alvaro Product: Montoro)',
index: 0,
mapping: 'standard',
timestamp: 5200,
vibrationActuator: {
playEffect: (type, obj) => `${type} - ${obj.duration}`
}
},
{
axes: [1.0, 1.0],
buttons: [
{ pressed: true, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: true, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 }
],
connected: true,
id: '10-button gamepad mockup (Vendor: Alvaro Product: Montoro10)',
index: 1,
mapping: '',
timestamp: 5200,
vibrationActuator: null
},
{
axes: [],
buttons: [],
connected: true,
id: 'No-button No-axe gamepad mockup (Vendor: Alvaro Product: Montoro10)',
index: 2,
mapping: '',
timestamp: 5200,
vibrationActuator: { playEffect: 'error' }
}
];
const gamepadsFirefox = [
{
axes: [0, 0, 0, 0, 0],
buttons: [
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 }
],
connected: true,
id: '17-button gamepad mockup (STANDARD Vendor: Alvaro Product: Montoro)',
index: 0,
mapping: 'standard',
timestamp: 5200,
hapticActuators: {
pulse: (intensity, duration) => `vibrate at ${intensity} for ${duration}ms`
}
},
{
axes: [0, 0, 0],
buttons: [
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 }
],
connected: true,
id: '10-button gamepad mockup (Vendor: Alvaro Product: Montoro10)',
index: 1,
mapping: '',
timestamp: 5200,
hapticActuators: null
},
{
axes: [0, 0, 0],
buttons: [
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 },
{ pressed: false, touched: false, value: 0 }
],
connected: true,
id: '10-button gamepad mockup (Vendor: Alvaro Product: Montoro10)',
index: 2,
mapping: '',
timestamp: 5200,
hapticActuators: [
{
pulse: (intensity, duration) => `vibrate at ${intensity} for ${duration}ms`
},
null
]
},
{
axes: [0, 0, 0],
buttons: [{ pressed: false, touched: false, value: 0 }],
connected: true,
id: '1-button gamepad mockup (Vendor: Alvaro Product: Montoro10)',
index: 3,
mapping: '',
timestamp: 5200,
hapticActuators: [
{
pulse: 'error'
},
null
]
}
];
export { gamepads, gamepadsFirefox };
================================================
FILE: tests/tools.test.js
================================================
import { isGamepadSupported, log, error, emptyEvents } from '../src/tools';
import { gamepads } from './mock.gamepads';
describe('log', () => {
it('log works without parameters', () => {
log('Sentence test');
});
it('error log works', () => {
log('Error sentence test', 'error');
});
it('log works with different parameters', () => {
log('Info sentence test', 'info');
log('Log sentence test', 'log');
});
it('error log works', () => {
error('Error sentence test');
});
it('branch test console and error', () => {
const auxconsole = console;
console = null;
log('Console sentence test');
log('Error sentence test', 'error');
log('Info sentence test', 'info');
log('Info sentence test', 'log');
error('Error sentence test');
console = auxconsole;
});
});
describe('isGamepadSupported', () => {
it('check if gamepad is supported', () => {
const aux = isGamepadSupported();
expect(aux).toEqual(false);
});
it('check if gamepad is supported', () => {
const mockGamepads = () => gamepads;
global.navigator.getGamepads = mockGamepads;
const aux1 = isGamepadSupported();
expect(aux1).toEqual(true);
global.navigator.getGamepads = 'error';
const aux2 = isGamepadSupported();
expect(aux2).toEqual(false);
});
it('check if gamepad is supported (webkit)', () => {
const webkitGetGamepads = () => gamepads;
global.navigator.webkitGetGamepads = webkitGetGamepads;
const aux = isGamepadSupported();
expect(aux).toEqual(true);
global.navigator.webkitGetGamepads = 'error';
const aux2 = isGamepadSupported();
expect(aux2).toEqual(false);
});
});
describe('emptyEvents', () => {
it('check all events return to be an object', () => {
const aux = emptyEvents();
expect(typeof aux).toEqual('object');
expect(typeof aux.action).toEqual('function');
expect(typeof aux.before).toEqual('function');
expect(typeof aux.after).toEqual('function');
});
});