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:

Waiting...

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:

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:

The available button aliases are:

================================================ 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

Alvanoid

Points: 0
================================================ FILE: examples/example-6-multiplayer.html ================================================ GameControl Example: Multiplayer (Pong)
0
0
================================================ 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).

1.00

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.

How it works

Vibration of the gamepad can be triggered using the .vibrate() method of the gamepad object. That method takes two parameters:

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. [![Build Status](https://travis-ci.org/alvaromontoro/gamecontroller.js.svg?branch=master)](https://travis-ci.org/alvaromontoro/gamecontroller.js) [![npm](https://img.shields.io/npm/v/gamecontroller.js.svg)](https://www.npmjs.com/package/gamecontroller.js) [![npm](https://img.shields.io/npm/l/gamecontroller.js.svg)](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): ![https://github.com/alvaromontoro/gamecontroller.js/blob/master/public/gamepad.svg](https://github.com/alvaromontoro/gamecontroller.js/blob/master/public/gamepad.svg) ## Browser Support | ![Edge Logo 32x32](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/56.3.2/edge/edge_32x32.png)
Edge | ![Firefox Logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/56.3.2/archive/firefox_23-56/firefox_23-56_32x32.png)
Firefox | ![Chrome](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/56.3.2/archive/chrome_12-48/chrome_12-48_32x32.png)
Chrome | ![Safari Logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/56.3.2/archive/safari_1-7/safari_1-7_32x32.png)
Safari | ![Opera logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/56.3.2/opera/opera_32x32.png)
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'); }); });