├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── index.html └── src │ ├── index.js │ └── style.css ├── package.json ├── rollup.config.js └── src └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .DS_Store 4 | package-lock.json 5 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Colin van Eenige 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unswitch 2 | 3 | [![npm version](https://img.shields.io/npm/v/unswitch.svg)](https://www.npmjs.com/package/unswitch) 4 | [![gzip size](http://img.badgesize.io/https://unpkg.com/unswitch/dist/unswitch.es.js?compression=gzip)](https://unpkg.com/unswitch) 5 | [![license](https://img.shields.io/npm/l/unswitch.svg)](https://github.com/vaneenige/unswitch/blob/master/LICENSE) 6 | [![dependencies](https://img.shields.io/badge/dependencies-none-ff69b4.svg)](https://github.com/vaneenige/unswitch/blob/master/package.json) 7 | 8 | Unswitch is a tiny event handler for Switch controllers on the web based on the Gamepad API. Attach callbacks to button presses (up & down) and the joystick! 9 | 10 | ## Install 11 | 12 | ``` 13 | $ npm install --save unswitch 14 | ``` 15 | 16 | > Use script tags or modules? Check out the version on [unpkg](https://unpkg.com/unswitch)! 17 | 18 | ## Setup 19 | Connecting a Switch controller is easy: pair it with bluetooth and you're ready to go! This library doesn't listen to `connected` or `disconnected` events but in some cases it might be useful. Here's how you can listen to these events: 20 | 21 | ```js 22 | window.addEventListener("gamepadconnected", ({ gamepad }) => {}); 23 | ``` 24 | 25 | ```js 26 | window.addEventListener("gamepaddisconnected", ({ gamepad }) => {}); 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```js 32 | // Import the library 33 | import Unswitch from 'unswitch'; 34 | 35 | // Observe a controller 36 | const unswitch = new Unswitch({ 37 | side: 'L', // or R 38 | buttons: (button, pressed, side) => console.log(`Catch-all - button: ${button} was ${pressed ? 'pressed' : 'released'} on the ${side} side`), 39 | b: p => {}, 40 | a: p => {}, 41 | y: p => {}, 42 | x: p => {}, 43 | sl: p => {}, 44 | sr: p => {}, 45 | minus: p => {}, 46 | plus: p => {}, 47 | lstick: p => {}, 48 | rstick: p => {}, 49 | home: p => {}, 50 | screenshot: p => {}, 51 | bumper: p => {}, // L or R 52 | trigger: p => {}, // ZL or ZR 53 | axes: p => {}, 54 | }); 55 | 56 | function render() { 57 | // Call the update function manually 58 | unswitch.update(); 59 | requestAnimationFrame(render); 60 | } 61 | 62 | render(); 63 | ``` 64 | 65 | Please note that it's not required to pass all button-functions to Unswitch and will only be executed when you provide them. 66 | You are able to use the `buttons` function to catch all button presses and implement your own logic using the provided data. The `buttons` function will always be executed when provided, even when the button is also passed as property. 67 | 68 | It's possible to connect up to two controllers at the same time. To make this work `side` is to be passed with either `L` (left) or `R` (right) for the controllers respectively. Calling `unswitch.update()` will check every button for a change in state. If a callback is provided the new state is passed along. The axis works in the same way, but instead of a `boolean` it will return a `number` from 0 to 8. Number 0 to 7 are for the joystick positions going clockwise, number 8 is used as default (center). 69 | 70 | > Have a look at the demo on [Codepen](https://codepen.io/cvaneenige/pen/gjdJrP)! 71 | 72 | ## Contribute 73 | 74 | Are you excited about this library and have interesting ideas on how to improve it? Please tell me or contribute directly! 🙌 75 | 76 | ``` 77 | npm install > npm run demo > http://localhost:8080 78 | ``` 79 | 80 | ## License 81 | 82 | MIT © Colin van Eenige 83 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Demo 5 | 6 | 7 | 8 | 9 | 10 | 11 |

Event history:

12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import Unswitch from '../../src/index'; 2 | 3 | const ul = document.querySelector('ul'); 4 | 5 | function lb(side, control, state) { 6 | ul.innerHTML = `
  • ${side} ${control} was ${state ? 'pressed' : 'released'}
  • ${ul.innerHTML}`; 7 | } 8 | 9 | function la(controller, position) { 10 | ul.innerHTML = `
  • ${controller} ~ Axes position ${position}
  • ${ul.innerHTML}`; 11 | } 12 | 13 | function catchAll(button, pressed, side) { 14 | const enableCatchAll = false; 15 | if (enableCatchAll) ul.innerHTML = `
  • Catch-all - button: ${button} was ${pressed ? 'pressed' : 'released'} on the ${side} side
  • ${ul.innerHTML}`; 16 | } 17 | 18 | function createController(s) { 19 | const controller = new Unswitch({ 20 | side: s, 21 | buttons: catchAll, 22 | b: p => lb(s, ' ~ B', p), 23 | a: p => lb(s, ' ~ A', p), 24 | y: p => lb(s, ' ~ Y', p), 25 | x: p => lb(s, ' ~ X', p), 26 | sl: p => lb(s, ' ~ SL', p), 27 | sr: p => lb(s, ' ~ SR', p), 28 | minus: p => lb(s, ' ~ Minus ', p), 29 | plus: p => lb(s, ' ~ Plus', p), 30 | lstick: p => lb(s, ' ~ Lstick', p), 31 | rstick: p => lb(s, ' ~ Rstick', p), 32 | home: p => lb(s, ' ~ Home', p), 33 | screenshot: p => lb(s, ' ~ Screenshot', p), 34 | bumper: p => lb(s, ' ~ Bumper', p), // L or R 35 | trigger: p => lb(s, ' ~ Trigger', p), // ZL or ZR 36 | axes: p => la(s, p), 37 | }); 38 | return controller; 39 | } 40 | 41 | const controllerLeft = createController('L'); 42 | const controllerRight = createController('R'); 43 | 44 | function render() { 45 | controllerLeft.update(); 46 | controllerRight.update(); 47 | requestAnimationFrame(render); 48 | } 49 | 50 | render(); 51 | -------------------------------------------------------------------------------- /demo/src/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 20px; 5 | width: 100%; 6 | height: 100%; 7 | 8 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 9 | letter-spacing: 0; 10 | font-style: normal; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | h1 { 17 | font-weight: 500; 18 | color: #00BBDD; 19 | } 20 | 21 | ul { 22 | list-style: none; 23 | padding: 0; 24 | max-height: 220px; 25 | overflow: auto; 26 | } 27 | 28 | li { 29 | line-height: 24px; 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unswitch", 3 | "version": "1.4.0", 4 | "description": "A tiny event handler for switch controllers on the web.", 5 | "main": "dist/unswitch.es.js", 6 | "unpkg": "dist/unswitch.iife.js", 7 | "scripts": { 8 | "demo": "cross-env DEBUG=true rollup -c -w", 9 | "build": "npm run es && npm run iife", 10 | "es": "rollup -c -f es -o $npm_package_main && gzip-size $npm_package_main", 11 | "iife": "rollup -c -f iife -o $npm_package_unpkg && gzip-size $npm_package_unpkg", 12 | "prepare": "npm run build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/vaneenige/unswitch.git" 17 | }, 18 | "license": "MIT", 19 | "author": { 20 | "name": "Colin van Eenige", 21 | "email": "cvaneenige@gmail.com", 22 | "url": "https://use-the-platform.com" 23 | }, 24 | "files": [ 25 | "src", 26 | "dist" 27 | ], 28 | "keywords": [ 29 | "controller", 30 | "input" 31 | ], 32 | "devDependencies": { 33 | "babel-core": "^6.26.3", 34 | "babel-plugin-external-helpers": "^6.22.0", 35 | "babel-preset-env": "^1.7.0", 36 | "cross-env": "^5.2.0", 37 | "eslint": "^4.19.1", 38 | "eslint-config-airbnb-base": "^13.0.0", 39 | "eslint-plugin-import": "^2.13.0", 40 | "gzip-size-cli": "^3.0.0", 41 | "rollup": "^0.62.0", 42 | "rollup-plugin-babel": "^3.0.7", 43 | "rollup-plugin-eslint": "^4.0.0", 44 | "rollup-plugin-serve": "^0.4.2", 45 | "rollup-plugin-uglify": "^3.0.0" 46 | }, 47 | "eslintConfig": { 48 | "extends": "airbnb-base", 49 | "env": { 50 | "browser": true 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import uglify from 'rollup-plugin-uglify'; 3 | import serve from 'rollup-plugin-serve'; 4 | import eslint from 'rollup-plugin-eslint'; 5 | 6 | const { DEBUG } = process.env; 7 | 8 | const uglifySettings = { 9 | compress: { 10 | negate_iife: false, 11 | unsafe_comps: true, 12 | properties: true, 13 | keep_fargs: false, 14 | pure_getters: true, 15 | collapse_vars: true, 16 | unsafe: true, 17 | warnings: false, 18 | sequences: true, 19 | dead_code: true, 20 | drop_debugger: true, 21 | comparisons: true, 22 | conditionals: true, 23 | evaluate: true, 24 | booleans: true, 25 | loops: true, 26 | unused: true, 27 | hoist_funs: true, 28 | if_return: true, 29 | join_vars: true, 30 | drop_console: true, 31 | pure_funcs: ['classCallCheck', 'invariant', 'warning'], 32 | }, 33 | output: { 34 | comments: false, 35 | }, 36 | mangle: { 37 | toplevel: true, 38 | reserved: ['Unswitch'], 39 | }, 40 | }; 41 | 42 | const input = DEBUG ? './demo/src/index.js' : './src/index.js'; 43 | 44 | const output = { 45 | file: DEBUG ? './demo/dist/bundle.js' : './dist/unswitch.es.js', 46 | format: 'es', 47 | name: 'Unswitch', 48 | sourcemap: false, 49 | }; 50 | 51 | const plugins = [ 52 | /** 53 | * Verify entry point and all imported files with ESLint. 54 | * @see https://github.com/TrySound/rollup-plugin-eslint 55 | */ 56 | eslint({ throwOnError: true }), 57 | /** 58 | * Convert ES2015 with babel. 59 | * @see https://github.com/rollup/rollup-plugin-babel 60 | */ 61 | babel({ 62 | babelrc: false, 63 | presets: [['env', { loose: true, modules: false }]], 64 | plugins: ['external-helpers'], 65 | }), 66 | ]; 67 | 68 | if (DEBUG) { 69 | plugins.push( 70 | /** 71 | * Serve the bundle for local debugging. 72 | * @see https://github.com/thgh/rollup-plugin-serve 73 | */ 74 | serve({ 75 | port: 8080, 76 | contentBase: 'demo', 77 | }), 78 | ); 79 | } else { 80 | plugins.push( 81 | /** 82 | * Rollup plugin to minify generated bundle. 83 | * @see https://github.com/TrySound/rollup-plugin-uglify 84 | */ 85 | uglify(uglifySettings), 86 | ); 87 | } 88 | 89 | export default { input, output, plugins }; 90 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const buttonMappings = ['a', 'x', 'b', 'y', 'sl', 'sr', '-', '-', 'minus', 'plus', 'lstick', 'rstick', 'home', 'screenshot', 'bumper', 'trigger']; 2 | 3 | /** 4 | * Get the axes position based based on browser. 5 | * @param {array} axes 6 | * @param {array} buttons 7 | */ 8 | function getAxesPosition(axes, buttons) { 9 | if (axes.length === 10) { 10 | return Math.round(axes[9] / (2 / 7) + 3.5); 11 | } 12 | const [right, left, down, up] = [...buttons].reverse(); 13 | const buttonValues = [up, right, down, left] 14 | .map((pressed, i) => (pressed.value ? i * 2 : false)) 15 | .filter(val => val !== false); 16 | if (buttonValues.length === 0) return 8; 17 | if (buttonValues.length === 2 && buttonValues[0] === 0 && buttonValues[1] === 6) return 7; 18 | return buttonValues.reduce((prev, curr) => prev + curr, 0) / buttonValues.length; 19 | } 20 | 21 | /** 22 | * Create an instance of Unswitch. 23 | * @param {object} settings 24 | */ 25 | function Unswitch(settings) { 26 | const buttonState = {}; 27 | let axesPosition = 8; 28 | 29 | for (let i = buttonMappings.length - 1; i >= 0; i -= 1) { 30 | buttonState[buttonMappings[i]] = { pressed: false }; 31 | } 32 | 33 | this.update = () => { 34 | const gamepads = navigator.getGamepads(); 35 | for (let i = Object.keys(gamepads).length - 1; i >= 0; i -= 1) { 36 | if (gamepads[i] && gamepads[i].id && gamepads[i].id.indexOf(settings.side) !== -1) { 37 | this.observe(gamepads[i]); 38 | break; 39 | } 40 | } 41 | }; 42 | 43 | this.observe = (pad) => { 44 | const { buttons, axes } = pad; 45 | for (let j = buttonMappings.length - 1; j >= 0; j -= 1) { 46 | const button = buttonMappings[j]; 47 | if (buttonState[button].pressed !== buttons[j].pressed) { 48 | buttonState[button].pressed = buttons[j].pressed; 49 | if (settings[button]) { 50 | settings[button](buttonState[button].pressed); 51 | } 52 | 53 | if (settings.buttons) { 54 | settings.buttons(button, buttonState[button].pressed, settings.side); 55 | } 56 | } 57 | } 58 | if (settings.axes) { 59 | const position = getAxesPosition(axes, buttons); 60 | if (position !== axesPosition) { 61 | settings.axes(position); 62 | axesPosition = position; 63 | } 64 | } 65 | }; 66 | } 67 | 68 | export default Unswitch; 69 | --------------------------------------------------------------------------------