├── .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 | [](https://www.npmjs.com/package/unswitch)
4 | [](https://unpkg.com/unswitch)
5 | [](https://github.com/vaneenige/unswitch/blob/master/LICENSE)
6 | [](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 |
--------------------------------------------------------------------------------