├── .gitignore ├── public ├── images │ ├── noise.png │ ├── sprites.png │ ├── touchpad_icons │ │ ├── icon.png │ │ ├── icon16.png │ │ ├── favicon.ico │ │ ├── apple-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── ms-icon-70x70.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── apple-icon-precomposed.png │ │ ├── browserconfig.xml │ │ └── manifest.json │ ├── keyboard_icons │ │ ├── favicon.ico │ │ ├── apple-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── ms-icon-70x70.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── apple-icon-precomposed.png │ │ ├── browserconfig.xml │ │ └── manifest.json │ ├── gamepad_icons │ │ ├── favicon_128.png │ │ ├── favicon_32.png │ │ ├── favicon_64.png │ │ ├── launcher-icon-1x.png │ │ ├── launcher-icon-2x.png │ │ ├── launcher-icon-3x.png │ │ ├── launcher-icon-4x.png │ │ ├── launcher-icon-0-75x.png │ │ ├── launcher-icon-1-5x.png │ │ └── manifest.json │ └── manifest.json ├── js │ ├── common.js │ ├── index.js │ ├── keyboard │ │ ├── utils.js │ │ ├── input.js │ │ └── settings.js │ ├── virtual_keyboard_client.js │ ├── lib │ │ ├── domReady.js │ │ └── require.js │ ├── virtualjoystick.js │ ├── virtual_touchpad_client.js │ └── virtual_gamepad_client.js ├── index.html ├── css │ ├── touchpad.css │ └── style.css ├── keyboard.html └── touchpad.html ├── config.json ├── .github └── pull_request_template.md ├── package.json ├── app ├── virtual_keyboard_hub.coffee ├── virtual_touchpad_hub.coffee ├── virtual_keyboard_hub.js ├── virtual_touchpad_hub.js ├── virtual_gamepad_hub.coffee ├── virtual_gamepad_hub.js ├── virtual_keyboard.coffee ├── virtual_gamepad.coffee ├── virtual_keyboard.js ├── virtual_touchpad.coffee ├── virtual_gamepad.js └── virtual_touchpad.js ├── lib ├── log.js ├── uinput_structs.js ├── uinput.js └── io.js ├── main.coffee ├── LICENSE ├── TROUBLESHOOTING.md ├── CHANGELOG.md ├── main.js ├── README_CONFIG.md ├── CONTRIBUTING.md ├── CREATE_KEYBOARD_LAYOUT.md ├── server.coffee ├── README.md └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /public/images/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/noise.png -------------------------------------------------------------------------------- /public/images/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/sprites.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/icon.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/icon16.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/favicon.ico -------------------------------------------------------------------------------- /public/images/touchpad_icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/favicon.ico -------------------------------------------------------------------------------- /public/images/gamepad_icons/favicon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/gamepad_icons/favicon_128.png -------------------------------------------------------------------------------- /public/images/gamepad_icons/favicon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/gamepad_icons/favicon_32.png -------------------------------------------------------------------------------- /public/images/gamepad_icons/favicon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/gamepad_icons/favicon_64.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/apple-icon.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/apple-icon.png -------------------------------------------------------------------------------- /public/js/common.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | 'jquery': './lib/jquery', 4 | 'socketio': './lib/socketio' 5 | } 6 | }); -------------------------------------------------------------------------------- /public/images/keyboard_icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/favicon-96x96.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/favicon-96x96.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/images/gamepad_icons/launcher-icon-1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/gamepad_icons/launcher-icon-1x.png -------------------------------------------------------------------------------- /public/images/gamepad_icons/launcher-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/gamepad_icons/launcher-icon-2x.png -------------------------------------------------------------------------------- /public/images/gamepad_icons/launcher-icon-3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/gamepad_icons/launcher-icon-3x.png -------------------------------------------------------------------------------- /public/images/gamepad_icons/launcher-icon-4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/gamepad_icons/launcher-icon-4x.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/images/gamepad_icons/launcher-icon-0-75x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/gamepad_icons/launcher-icon-0-75x.png -------------------------------------------------------------------------------- /public/images/gamepad_icons/launcher-icon-1-5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/gamepad_icons/launcher-icon-1-5x.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/android-icon-36x36.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/android-icon-48x48.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/android-icon-72x72.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/android-icon-96x96.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/android-icon-36x36.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/android-icon-48x48.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/android-icon-72x72.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/android-icon-96x96.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/android-icon-144x144.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/android-icon-192x192.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/android-icon-144x144.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/android-icon-192x192.png -------------------------------------------------------------------------------- /public/images/keyboard_icons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/keyboard_icons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/images/touchpad_icons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jehervy/node-virtual-gamepads/HEAD/public/images/touchpad_icons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 80, 3 | "useGamepadByDefault": false, 4 | "analog": true, 5 | "logLevel": "info", 6 | "ledBitFieldSequence": [1,2,4,8,9,10,12,13,14,15] 7 | } 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Make sure you've read the [contribution guideline](https://github.com/jehervy/node-virtual-gamepads/blob/develop/CONTRIBUTING.md)! 2 | When you did, you can delete this text. -------------------------------------------------------------------------------- /public/images/keyboard_icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #efefef -------------------------------------------------------------------------------- /public/images/touchpad_icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #4c4c4c -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virtual-gamepads", 3 | "version": "1.5.0", 4 | "description": "Virtual gamepads application", 5 | "main": "main.js", 6 | "author": "miroof", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/miroof/node-virtual-gamepad" 11 | }, 12 | "dependencies": { 13 | "express": "^4.18.2", 14 | "forever-monitor": "^3.0.3", 15 | "ioctl": "^2.0.2", 16 | "ref-array-napi": "^1.2.2", 17 | "ref-napi": "^1.3.5", 18 | "ref-struct-napi": "^1.1.0", 19 | "socket.io": "^4.7.2", 20 | "winston": "^3.11.0" 21 | }, 22 | "devDependencies": { 23 | "coffeescript": "1.10.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/images/keyboard_icons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Virtual Keyboard", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ], 41 | "start_url": "/keyboard.html", 42 | "display": "standalone", 43 | "orientation": "landscape" 44 | } -------------------------------------------------------------------------------- /public/images/touchpad_icons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Virtual Touchpad", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ], 41 | "start_url": "/touchpad.html", 42 | "display": "standalone", 43 | "orientation": "landscape" 44 | } -------------------------------------------------------------------------------- /app/virtual_keyboard_hub.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Created by roba91 on 15/08/2016 3 | Virtual keyboard hub class 4 | ### 5 | 6 | keyboard = require './virtual_keyboard' 7 | log = require '../lib/log' 8 | 9 | class virtual_keyboard_hub 10 | 11 | constructor: () -> 12 | @keyboards = [] 13 | 14 | connectKeyboard: (callback) -> 15 | boardId = @keyboards.length 16 | 17 | # Create and connect the keyboard 18 | log 'info', 'Creating and connecting to keyboard number' + boardId 19 | @keyboards[boardId] = new keyboard() 20 | @keyboards[boardId].connect () -> 21 | callback boardId 22 | , (err) -> 23 | log 'error', "Couldn't connect to keyboard:\n" + JSON.stringify(err) 24 | callback -1 25 | 26 | disconnectKeyboard: (boardId, callback) -> 27 | if @keyboards[boardId] 28 | @keyboards[boardId].disconnect () => 29 | @keyboards[boardId] = undefined 30 | callback() 31 | 32 | sendEvent: (boardId, event) -> 33 | if @keyboards[boardId] 34 | @keyboards[boardId].sendEvent event 35 | 36 | module.exports = virtual_keyboard_hub 37 | -------------------------------------------------------------------------------- /app/virtual_touchpad_hub.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Virtual touchpad hub class 3 | ### 4 | 5 | touchpad = require './virtual_touchpad' 6 | log = require '../lib/log' 7 | 8 | class virtual_touchpad_hub 9 | 10 | constructor: () -> 11 | @touchpads = [] 12 | 13 | connectTouchpad: (callback) -> 14 | touchpadId = @touchpads.length 15 | 16 | # Create and connect the touchpad 17 | log 'info', 'Creating and connecting to touchpad number ' + touchpadId 18 | @touchpads[touchpadId] = new touchpad() 19 | @touchpads[touchpadId].connect () -> 20 | callback touchpadId 21 | , (err) -> 22 | log 'error', "Couldn't connect to touchpad:\n" + JSON.stringify(err) 23 | callback -1 24 | 25 | disconnectTouchpad: (touchpadId, callback) -> 26 | if @touchpads[touchpadId] 27 | @touchpads[touchpadId].disconnect () => 28 | @touchpads[touchpadId] = undefined 29 | callback() 30 | 31 | sendEvent: (touchpadId, event) -> 32 | if @touchpads[touchpadId] 33 | @touchpads[touchpadId].sendEvent event 34 | 35 | module.exports = virtual_touchpad_hub 36 | -------------------------------------------------------------------------------- /lib/log.js: -------------------------------------------------------------------------------- 1 | var config = require('../config.json'); 2 | var winston = require('winston'); 3 | 4 | 5 | var format = winston.format.printf(function(opts) { 6 | return '' + opts.timestamp + ' ' + opts.level + ': ' + opts.message; 7 | }); 8 | 9 | var logger = winston.createLogger({ 10 | levels: { 11 | panic: 0, 12 | error: 1, 13 | warning: 2, 14 | info: 3, 15 | verbose: 4, 16 | debug: 5 17 | }, 18 | transports: [ 19 | new (winston.transports.Console)({ 20 | format: winston.format.combine( 21 | winston.format.timestamp({ 22 | format: function () { 23 | var tzoffset = (new Date()).getTimezoneOffset() * 60000; //offset in milliseconds 24 | return (new Date(Date.now() - tzoffset)).toISOString().slice(0, -1); 25 | } 26 | }), 27 | format 28 | ) 29 | }) 30 | ], 31 | level: process.env.LOGLEVEL || config.logLevel 32 | }); 33 | 34 | logger.log('info', 'loglevel ' + logger.level); 35 | 36 | module.exports = logger.log.bind(logger); -------------------------------------------------------------------------------- /main.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Created by Robsdedude on 01/10/2017 3 | Virtual gamepad application 4 | ### 5 | 6 | forever = require('forever-monitor') 7 | log = require './lib/log' 8 | 9 | server = new (forever.Monitor)( 10 | require('path').resolve(__dirname, 'server.js'), { 11 | max: Infinity, 12 | args: [], 13 | }); 14 | 15 | exiting = false 16 | 17 | server.on 'exit', -> 18 | log 'warning', 'server.js has exited'; 19 | 20 | earlyDeathCount = 0 21 | server.on 'exit:code', -> 22 | return if exiting 23 | diedAfter = Date.now() - server.ctime 24 | log 'info', 'diedAfter: ' + diedAfter 25 | earlyDeathCount = if diedAfter < 5000 then earlyDeathCount+1 else 0 26 | log 'info', 'earlyDeathCount: ' + earlyDeathCount 27 | if earlyDeathCount >= 3 28 | log 'error', 'Died too often too fast.' 29 | server.stop() 30 | 31 | server.on 'restart', -> 32 | log 'error' ,'Forever restarting script for ' + server.times + ' time' 33 | 34 | 35 | for sig in ['SIGTERM', 'SIGINT', 'exit'] 36 | process.on sig, ((s) -> -> 37 | log 'info', 'received ' + s 38 | exiting = true 39 | server.stop() 40 | )(sig) 41 | 42 | 43 | server.start(); -------------------------------------------------------------------------------- /public/js/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by rouven on 25.02.17. 3 | */ 4 | 5 | require(["common"], function(common) { 6 | require([ 7 | "jquery", 8 | "lib/domReady"], 9 | function ($, domReady) { 10 | $(".slide-link").click(function(e) { 11 | if ( 12 | e.ctrlKey || 13 | e.shiftKey || 14 | e.metaKey || // apple 15 | (e.button && e.button == 1) // middle click, >IE9 + everyone else 16 | ){ 17 | return; 18 | } 19 | e.preventDefault(); 20 | 21 | var target = $(this).attr('href'); 22 | 23 | $('body').bind("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", function(){ 24 | window.location = target; 25 | }).addClass('slide-left'); 26 | 27 | return false; 28 | }); 29 | if (location.href.match(/\?analog$/)){ 30 | $('[href="gamepad.html"]').attr('href', 'gamepad.html?analog'); 31 | } 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 miroof 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 | 23 | -------------------------------------------------------------------------------- /public/images/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Virtual Controller", 3 | "icons": [ 4 | { 5 | "src": "images/icons/launcher-icon-0-75x.png", 6 | "sizes": "36x36", 7 | "type": "image/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "images/icons/launcher-icon-1x.png", 12 | "sizes": "48x48", 13 | "type": "image/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "images/icons/launcher-icon-1-5x.png", 18 | "sizes": "72x72", 19 | "type": "image/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "images/icons/launcher-icon-2x.png", 24 | "sizes": "96x96", 25 | "type": "image/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "images/icons/launcher-icon-3x.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "images/icons/launcher-icon-4x.png", 36 | "sizes": "192x192", 37 | "type": "image/png", 38 | "density": "4.0" 39 | } 40 | ], 41 | "start_url": "/", 42 | "display": "standalone", 43 | "orientation": "landscape" 44 | } 45 | -------------------------------------------------------------------------------- /public/images/gamepad_icons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Virtual Gamepad", 3 | "icons": [ 4 | { 5 | "src": "images/icons/launcher-icon-0-75x.png", 6 | "sizes": "36x36", 7 | "type": "image/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "images/icons/launcher-icon-1x.png", 12 | "sizes": "48x48", 13 | "type": "image/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "images/icons/launcher-icon-1-5x.png", 18 | "sizes": "72x72", 19 | "type": "image/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "images/icons/launcher-icon-2x.png", 24 | "sizes": "96x96", 25 | "type": "image/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "images/icons/launcher-icon-3x.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "images/icons/launcher-icon-4x.png", 36 | "sizes": "192x192", 37 | "type": "image/png", 38 | "density": "4.0" 39 | } 40 | ], 41 | "start_url": "/gamepad.html", 42 | "display": "standalone", 43 | "orientation": "landscape" 44 | } 45 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Virtual Controls 9 | 10 | 11 | 12 | 13 | 14 | 15 | game pad 16 | 17 | 18 | 19 | 20 | keyboard 21 | 22 | 23 | 24 | 25 | touch pad 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | Troubleshooting 2 | =============== 3 | 4 | npm install 5 | ----------- 6 | If you get errors on 7 | 8 | npm install 9 | 10 | make sure you have node version 9 or higher installed. 11 | If you open an issue please provide version numbers for both nodejs and npm and 12 | state what OS (including version) you're working with. 13 | 14 | ### installing `ioctl` fails 15 | Errors along the line of 16 | ``` 17 | ValueError: invalid mode: 'rU' while trying to load binding.gyp 18 | ``` 19 | can occur if you have Python 3.11 or newer installed while running on older node verions. 20 | Try downgrading to Python 3.10 or older or upgrading nodejs. 21 | 22 | 23 | Error: EINVAL, invalid argument 24 | ------------------------------- 25 | If you get errors on running 26 | 27 | sudo node main.js 28 | 29 | that look something like 30 | 31 | Missing error handler on `socket`. 32 | Error: EINVAL, invalid argument 33 | 34 | this may be caused by running on ubuntu or a system that does not come with 35 | header-files (dev-packages) pre-installed. Try: 36 | 37 | sudo apt-get install libudev-dev 38 | 39 | 40 | Nothing helped 41 | -------------- 42 | If nothing helped see if you can find you problem in the 43 | [issue section](https://github.com/miroof/node-virtual-gamepads/issues?utf8=%E2%9C%93&q=). 44 | Still no solution? Open an issue. 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Upcoming 2 | ======== 3 | 4 | 1.5.0 5 | ===== 6 | * Allow theoretically infinitely many gamepads to connect to the server. 7 | Adding a setting in `config.json` to set the LED combination for each 8 | gamepad. 9 | * Update dependencies to also support nodejs version 12. 10 | * Allow server to be started from outside its containing folder 11 | * Bug fixes: 12 | * Fix some touch browsers registering double click on keyboard keys 13 | * Disable context menu (e.g. when long touching on android) 14 | * Update docs 15 | * Added [contribution guide](CONTRIBUTING.md) 16 | * Update Readme "developing" section 17 | 18 | 1.4.0 19 | ===== 20 | * Fix error when disconnecting clients (missing argument for fs.close) 21 | * Using npm lock file to fix dependencies' versions 22 | * Allow log level to be set with environment variable `LOGLEVEL` 23 | * Adding timestamp to log output 24 | * Kill server process if main.js (monitoring) gets killed. 25 | This will **not work for** `SIGKILL`. When you forcefully kill the 26 | server, make sure to kill it's child processes as well if intended. 27 | * Improve logging 28 | * Bug fixes: 29 | * Crash on failing keyboard initialization 30 | * Update dependencies 31 | * Add physical gamepad support to the client 32 | 33 | 1.3.0 34 | ===== 35 | * Introduced Changelog 36 | * Improved documentation (e.g. explaining `config.json`) 37 | * D-Pad supports analog stick behaviour by default 38 | * Improved logging 39 | * Using module `forever-monitor` to restart the server if it crashes 40 | for some reason. 41 | * Bug fixes 42 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | 3 | /* 4 | Created by Robsdedude on 01/10/2017 5 | Virtual gamepad application 6 | */ 7 | 8 | (function() { 9 | var earlyDeathCount, exiting, forever, i, len, log, ref, server, sig; 10 | 11 | forever = require('forever-monitor'); 12 | 13 | log = require('./lib/log'); 14 | 15 | server = new forever.Monitor(require('path').resolve(__dirname, 'server.js'), { 16 | max: Infinity, 17 | args: [] 18 | }); 19 | 20 | exiting = false; 21 | 22 | server.on('exit', function() { 23 | return log('warning', 'server.js has exited'); 24 | }); 25 | 26 | earlyDeathCount = 0; 27 | 28 | server.on('exit:code', function() { 29 | var diedAfter; 30 | if (exiting) { 31 | return; 32 | } 33 | diedAfter = Date.now() - server.ctime; 34 | log('info', 'diedAfter: ' + diedAfter); 35 | earlyDeathCount = diedAfter < 5000 ? earlyDeathCount + 1 : 0; 36 | log('info', 'earlyDeathCount: ' + earlyDeathCount); 37 | if (earlyDeathCount >= 3) { 38 | log('error', 'Died too often too fast.'); 39 | return server.stop(); 40 | } 41 | }); 42 | 43 | server.on('restart', function() { 44 | return log('error', 'Forever restarting script for ' + server.times + ' time'); 45 | }); 46 | 47 | ref = ['SIGTERM', 'SIGINT', 'exit']; 48 | for (i = 0, len = ref.length; i < len; i++) { 49 | sig = ref[i]; 50 | process.on(sig, (function(s) { 51 | return function() { 52 | log('info', 'received ' + s); 53 | exiting = true; 54 | return server.stop(); 55 | }; 56 | })(sig)); 57 | } 58 | 59 | server.start(); 60 | 61 | }).call(this); 62 | -------------------------------------------------------------------------------- /app/virtual_keyboard_hub.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | 3 | /* 4 | Created by roba91 on 15/08/2016 5 | Virtual keyboard hub class 6 | */ 7 | 8 | (function() { 9 | var keyboard, log, virtual_keyboard_hub; 10 | 11 | keyboard = require('./virtual_keyboard'); 12 | 13 | log = require('../lib/log'); 14 | 15 | virtual_keyboard_hub = (function() { 16 | function virtual_keyboard_hub() { 17 | this.keyboards = []; 18 | } 19 | 20 | virtual_keyboard_hub.prototype.connectKeyboard = function(callback) { 21 | var boardId; 22 | boardId = this.keyboards.length; 23 | log('info', 'Creating and connecting to keyboard number' + boardId); 24 | this.keyboards[boardId] = new keyboard(); 25 | return this.keyboards[boardId].connect(function() { 26 | return callback(boardId); 27 | }, function(err) { 28 | log('error', "Couldn't connect to keyboard:\n" + JSON.stringify(err)); 29 | return callback(-1); 30 | }); 31 | }; 32 | 33 | virtual_keyboard_hub.prototype.disconnectKeyboard = function(boardId, callback) { 34 | if (this.keyboards[boardId]) { 35 | return this.keyboards[boardId].disconnect((function(_this) { 36 | return function() { 37 | _this.keyboards[boardId] = void 0; 38 | return callback(); 39 | }; 40 | })(this)); 41 | } 42 | }; 43 | 44 | virtual_keyboard_hub.prototype.sendEvent = function(boardId, event) { 45 | if (this.keyboards[boardId]) { 46 | return this.keyboards[boardId].sendEvent(event); 47 | } 48 | }; 49 | 50 | return virtual_keyboard_hub; 51 | 52 | })(); 53 | 54 | module.exports = virtual_keyboard_hub; 55 | 56 | }).call(this); 57 | -------------------------------------------------------------------------------- /app/virtual_touchpad_hub.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | 3 | /* 4 | Virtual touchpad hub class 5 | */ 6 | 7 | (function() { 8 | var log, touchpad, virtual_touchpad_hub; 9 | 10 | touchpad = require('./virtual_touchpad'); 11 | 12 | log = require('../lib/log'); 13 | 14 | virtual_touchpad_hub = (function() { 15 | function virtual_touchpad_hub() { 16 | this.touchpads = []; 17 | } 18 | 19 | virtual_touchpad_hub.prototype.connectTouchpad = function(callback) { 20 | var touchpadId; 21 | touchpadId = this.touchpads.length; 22 | log('info', 'Creating and connecting to touchpad number ' + touchpadId); 23 | this.touchpads[touchpadId] = new touchpad(); 24 | return this.touchpads[touchpadId].connect(function() { 25 | return callback(touchpadId); 26 | }, function(err) { 27 | log('error', "Couldn't connect to touchpad:\n" + JSON.stringify(err)); 28 | return callback(-1); 29 | }); 30 | }; 31 | 32 | virtual_touchpad_hub.prototype.disconnectTouchpad = function(touchpadId, callback) { 33 | if (this.touchpads[touchpadId]) { 34 | return this.touchpads[touchpadId].disconnect((function(_this) { 35 | return function() { 36 | _this.touchpads[touchpadId] = void 0; 37 | return callback(); 38 | }; 39 | })(this)); 40 | } 41 | }; 42 | 43 | virtual_touchpad_hub.prototype.sendEvent = function(touchpadId, event) { 44 | if (this.touchpads[touchpadId]) { 45 | return this.touchpads[touchpadId].sendEvent(event); 46 | } 47 | }; 48 | 49 | return virtual_touchpad_hub; 50 | 51 | })(); 52 | 53 | module.exports = virtual_touchpad_hub; 54 | 55 | }).call(this); 56 | -------------------------------------------------------------------------------- /lib/uinput_structs.js: -------------------------------------------------------------------------------- 1 | // note: underscores instead of camel casing was used in variable names. This 2 | // was done to keep the name consistent with the underlying c/c++ files. 3 | 4 | var ref = require('ref-napi'); 5 | var io = require('./io'); 6 | var ArrayType = require('ref-array-napi'); 7 | var StructType = require('ref-struct-napi'); 8 | var uinput = require('./uinput'); 9 | 10 | var uinputStructs = {}; 11 | 12 | uinputStructs.time_t = ref.types.long; 13 | uinputStructs.suseconds_t = ref.types.long; 14 | 15 | uinputStructs.timeval = StructType({ 16 | tv_sec: uinputStructs.time_t, 17 | tv_usec: uinputStructs.suseconds_t 18 | }); 19 | 20 | uinputStructs.input_event = StructType({ 21 | time: uinputStructs.timeval, 22 | type: ref.types.uint16, 23 | code: ref.types.uint16, 24 | value: ref.types.int32 25 | }); 26 | 27 | uinputStructs.input_id = StructType({ 28 | bustype: ref.types.uint16, 29 | vendor: ref.types.uint16, 30 | product: ref.types.uint16, 31 | version: ref.types.uint16 32 | }); 33 | 34 | uinputStructs.uinput_setup = StructType({ 35 | id: uinputStructs.input_id, 36 | name: ArrayType(ref.types.char, uinput.UINPUT_MAX_NAME_SIZE), 37 | ff_effects_max: ref.types.uint32 38 | }); 39 | 40 | uinputStructs.uinput_user_dev = StructType({ 41 | name: ArrayType(ref.types.char, uinput.UINPUT_MAX_NAME_SIZE), 42 | id: uinputStructs.input_id, 43 | ff_effects_max: ref.types.uint32, 44 | absmax: ArrayType(ref.types.int32, uinput.ABS_CNT), 45 | absmin: ArrayType(ref.types.int32, uinput.ABS_CNT), 46 | absfuzz: ArrayType(ref.types.int32, uinput.ABS_CNT), 47 | absflat: ArrayType(ref.types.int32, uinput.ABS_CNT) 48 | }); 49 | 50 | uinputStructs.UI_DEV_SETUP = io._IOW( io.UINPUT_IOCTL_BASE, 3, uinputStructs.uinput_setup); 51 | 52 | module.exports = uinputStructs; 53 | -------------------------------------------------------------------------------- /README_CONFIG.md: -------------------------------------------------------------------------------- 1 | About config.json 2 | ================= 3 | 4 | You can customize the behaviour of the program by changing the values in 5 | `config.json`. This document explains what the single fields will do. 6 | 7 | * `port`: sets the port the web-server is listening on. 8 | * `useGamepadByDefault`: if set to `false`, `/` will redirect to a 9 | page where one of gamepad, keyboard, or touchpad can be chosen. 10 | If set to `true`, `/` redirects to the gamepad. The input-selection 11 | page can still be accessed via `/index.html`. 12 | * `analog`: if set to `true` the above-mentioned redirection will 13 | append `?analog` to the address. This flag will cause the gamepad's 14 | d-pad to act like an analog stick instead of d-pad. 15 | * `logLevel`: set it to `"debug"` to get a lot more logging output, 16 | to `"warning"` to only get critical output, or even to `"error"` if 17 | you want to only get errors logged (not recommended). 18 | * `ledBitFieldSequence`: must be an array of 'bit fields'. The length of the 19 | array will determine how many controllers can connect to the server while 20 | the bit field values will set what LED-combination of the controllers will 21 | be lit. The bit fields are numbers from 0-15, resulting in the following LED 22 | arrangements: 23 | ``` 24 | 0 - .... 25 | 1 - *... 26 | 2 - .*.. 27 | 3 - **.. 28 | 4 - ..*. 29 | 5 - *.*. 30 | 6 - .**. 31 | 7 - ***. 32 | 8 - ...* 33 | 9 - *..* 34 | 10 - .*.* 35 | 11 - **.* 36 | 12 - ..** 37 | 13 - *.** 38 | 14 - .*** 39 | 15 - **** 40 | ``` 41 | Example: `ledBitFieldSequence = [1, 12]` will allow at most two controllers 42 | to connect. The first controller will have only the first LED lit (`*...`) 43 | and the second controller will have the last two LEDs lit (`..**`). 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contribution Guideline 2 | ====================== 3 | 4 | Thank you for the interest and potential effort. 5 | Before you start to work on a pull request, please carefully read the following. 6 | 7 | * Look at the rest of the code and try to stick to the code style. Ugly code 8 | won't be merged. 9 | * If you plan to make a bigger change, you might want to open an issue first 10 | and discuss the idea and possible implementations with the developer team. 11 | Otherwise, it might turn out that your idea does not fit the gist of the 12 | project, your pull request gets rejected, and you feel like you've wasted 13 | your time. We don't want that! 14 | * Note that server-side code is mostly written in [CoffeeScript](https://coffeescript.org/). 15 | So don't change code in files like `xyz.js` if, in the same directory, there 16 | is a file like `xyz.coffee`. This means that the javascript file was 17 | generated by the CoffeeScript compiler. You can also see this by a comment at 18 | the beginning of the js file similar to `// Generated by CoffeeScript 1.10.0`. 19 | In this case you have to make the changes to the `.coffee` file(s) only and 20 | run the compiler to generate the `.js` file(s). 21 | * Usually you should file your pull request against the `develop` branch 22 | and most certainly not the `master` branch which is reserved for releases. 23 | * When you're done, don't forget to briefly describe the changes made in the 24 | [CHANGELOG](CHANGELOG.md#upcoming) under the section "Upcoming". 25 | 26 | For setting up the development environment check out the section 27 | [Developing in the README](README.md#developing). 28 | 29 | If you're having problems, you might find help on the 30 | [troubleshooting](TROUBLESHOOTING.md) page. If not feel free to open an issue or 31 | contact one of the developers. 32 | 33 | Thanks for reading. This will probably save you and us valuable time. -------------------------------------------------------------------------------- /public/js/keyboard/utils.js: -------------------------------------------------------------------------------- 1 | define(["jquery"], function ($) { 2 | 3 | $.fn.redraw = function(){ 4 | return $(this).each(function(){ 5 | var redraw = this.offsetHeight; 6 | }); 7 | }; 8 | 9 | return { 10 | //http://wowmotty.blogspot.de/2009/06/convert-jquery-rgb-output-to-hex-color.html 11 | rgb2hex: function (orig) { 12 | var rgb = orig.replace(/\s/g, '').match(/^rgba?\((\d+),(\d+),(\d+)/i); 13 | return (rgb && rgb.length === 4) ? "#" + 14 | ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) + 15 | ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2) + 16 | ("0" + parseInt(rgb[3], 10).toString(16)).slice(-2) : orig; 17 | }, 18 | 19 | //https://css-tricks.com/snippets/javascript/lighten-darken-color/ 20 | lightenDarkenColor: function (col, amt) { 21 | var usePound = false; 22 | if (col[0] == "#") { 23 | col = col.slice(1); 24 | usePound = true; 25 | } 26 | var num = parseInt(col, 16); 27 | 28 | var r = (num >> 16) + amt; 29 | if (r > 255) r = 255; 30 | else if (r < 0) r = 0; 31 | 32 | var b = ((num >> 8) & 0x00FF) + amt; 33 | if (b > 255) b = 255; 34 | else if (b < 0) b = 0; 35 | 36 | var g = (num & 0x0000FF) + amt; 37 | if (g > 255) g = 255; 38 | else if (g < 0) g = 0; 39 | 40 | return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16); 41 | 42 | }, 43 | 44 | parseKeyId: function(idString) { 45 | var isModKey = false; 46 | if (idString.search('m') == 0) { 47 | idString = idString.slice(1); 48 | isModKey = true; 49 | } 50 | return [isModKey, parseInt(idString)]; 51 | } 52 | } 53 | }); -------------------------------------------------------------------------------- /app/virtual_gamepad_hub.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Created by MIROOF on 04/03/2015 3 | Virtual gamepad hub class 4 | ### 5 | 6 | gamepad = require './virtual_gamepad' 7 | log = require '../lib/log' 8 | config = require '../config' 9 | 10 | num_gamepads = config.ledBitFieldSequence.length 11 | # ledBitFieldSequence should be an array of 'bit fields'. 12 | # the bit fields are numbers from 0-15, resulting in the following LED arrangements: 13 | # 0 - .... 14 | # 1 - *... 15 | # 2 - .*.. 16 | # 3 - **.. 17 | # 4 - ..*. 18 | # 5 - *.*. 19 | # 6 - .**. 20 | # 7 - ***. 21 | # 8 - ...* 22 | # 9 - *..* 23 | # 10 - .*.* 24 | # 11 - **.* 25 | # 12 - ..** 26 | # 13 - *.** 27 | # 14 - .*** 28 | # 15 - **** 29 | 30 | class virtual_gamepad_hub 31 | 32 | constructor: () -> 33 | @gamepads = [] 34 | for i in [0..(num_gamepads-1)] 35 | @gamepads[i] = undefined 36 | 37 | connectGamepad: (callback) -> 38 | padId = 0 39 | freeSlot = false 40 | 41 | # Check is a slot is available 42 | # and retrieve the corresponding gamepad id 43 | while !freeSlot and padId < num_gamepads 44 | if !@gamepads[padId] 45 | freeSlot = true 46 | else 47 | padId++ 48 | 49 | if !freeSlot 50 | log 'warning', "Couldn't add new gamepad: no slot left." 51 | callback -1 52 | else 53 | # Create and connect the gamepad 54 | log 'info', 'Creating and connecting to gamepad number ' + padId 55 | @gamepads[padId] = new gamepad() 56 | @gamepads[padId].connect () -> 57 | callback padId 58 | , (err) -> 59 | @gamepads[padId] = undefined 60 | log 'error', "Couldn't connect to gamepad:\n" + JSON.stringify(err) 61 | callback -1 62 | 63 | disconnectGamepad: (padId, callback) -> 64 | if @gamepads[padId] 65 | @gamepads[padId].disconnect () => 66 | @gamepads[padId] = undefined 67 | callback() 68 | 69 | sendEvent: (padId, event) -> 70 | if @gamepads[padId] 71 | @gamepads[padId].sendEvent event 72 | 73 | module.exports = virtual_gamepad_hub 74 | -------------------------------------------------------------------------------- /app/virtual_gamepad_hub.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | 3 | /* 4 | Created by MIROOF on 04/03/2015 5 | Virtual gamepad hub class 6 | */ 7 | 8 | (function() { 9 | var config, gamepad, log, num_gamepads, virtual_gamepad_hub; 10 | 11 | gamepad = require('./virtual_gamepad'); 12 | 13 | log = require('../lib/log'); 14 | 15 | config = require('../config'); 16 | 17 | num_gamepads = config.ledBitFieldSequence.length; 18 | 19 | virtual_gamepad_hub = (function() { 20 | function virtual_gamepad_hub() { 21 | var i, j, ref; 22 | this.gamepads = []; 23 | for (i = j = 0, ref = num_gamepads - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { 24 | this.gamepads[i] = void 0; 25 | } 26 | } 27 | 28 | virtual_gamepad_hub.prototype.connectGamepad = function(callback) { 29 | var freeSlot, padId; 30 | padId = 0; 31 | freeSlot = false; 32 | while (!freeSlot && padId < num_gamepads) { 33 | if (!this.gamepads[padId]) { 34 | freeSlot = true; 35 | } else { 36 | padId++; 37 | } 38 | } 39 | if (!freeSlot) { 40 | log('warning', "Couldn't add new gamepad: no slot left."); 41 | return callback(-1); 42 | } else { 43 | log('info', 'Creating and connecting to gamepad number ' + padId); 44 | this.gamepads[padId] = new gamepad(); 45 | return this.gamepads[padId].connect(function() { 46 | return callback(padId); 47 | }, function(err) { 48 | this.gamepads[padId] = void 0; 49 | log('error', "Couldn't connect to gamepad:\n" + JSON.stringify(err)); 50 | return callback(-1); 51 | }); 52 | } 53 | }; 54 | 55 | virtual_gamepad_hub.prototype.disconnectGamepad = function(padId, callback) { 56 | if (this.gamepads[padId]) { 57 | return this.gamepads[padId].disconnect((function(_this) { 58 | return function() { 59 | _this.gamepads[padId] = void 0; 60 | return callback(); 61 | }; 62 | })(this)); 63 | } 64 | }; 65 | 66 | virtual_gamepad_hub.prototype.sendEvent = function(padId, event) { 67 | if (this.gamepads[padId]) { 68 | return this.gamepads[padId].sendEvent(event); 69 | } 70 | }; 71 | 72 | return virtual_gamepad_hub; 73 | 74 | })(); 75 | 76 | module.exports = virtual_gamepad_hub; 77 | 78 | }).call(this); 79 | -------------------------------------------------------------------------------- /public/js/virtual_keyboard_client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by roba91 on 01.08.16. 3 | */ 4 | 5 | require(["common"], function(common) { 6 | require([ 7 | "jquery", 8 | "socketio", 9 | "keyboard/utils", 10 | "keyboard/settings", 11 | "keyboard/input"], 12 | function ($, io, utils, settings, input) { 13 | 14 | function showInput(str) { 15 | $('#inputOverlay').find('p').text(str).parent().addClass('active').redraw().removeClass('active'); 16 | } 17 | 18 | 19 | function loadKeyboardLayout(layout, cb) { 20 | var fn = settings.ALL_KEYBOARDS[layout]; 21 | if (fn == null) throw 'unknown layout: '+layout; 22 | fn = settings.KEYBOARDS_PATH+fn; 23 | var div = $('#keyboard-container'); 24 | $.get(fn, function (data) { 25 | var svg = $(data.rootElement); 26 | svg.removeAttr('height').removeAttr('width').attr('id', 'keyboard'); 27 | div.html(''); 28 | div.append(svg); 29 | if (cb != null) cb(); 30 | }); 31 | 32 | } 33 | 34 | 35 | function init(cb) { 36 | loadKeyboardLayout(settings.keyboardLayout, cb); 37 | } 38 | 39 | 40 | require(["lib/domReady"], function (domReady) { 41 | domReady(function () { 42 | var socket = io(); 43 | 44 | init(function () { 45 | $('.loader').hide(); 46 | socket.on("keyboardConnected", function(data) { 47 | input.listen(function (data) { // key callback 48 | socket.emit("boardEvent", data); 49 | }, function () { // settings callback 50 | settings.modal.open(); 51 | }); 52 | }); 53 | }); 54 | 55 | socket.on("connect", function() { 56 | socket.emit("connectKeyboard", null); 57 | }); 58 | 59 | socket.on("disconnect", function() { 60 | location.reload(); 61 | }); 62 | 63 | // disable context menu e.g. on long touches on android 64 | $(window).on("contextmenu", function(event) { 65 | event.preventDefault(); 66 | event.stopPropagation(); 67 | return false; 68 | }); 69 | }); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /lib/uinput.js: -------------------------------------------------------------------------------- 1 | var io = require('./io'); 2 | 3 | var uinput = {}; 4 | 5 | uinput.UI_SET_EVBIT = io._IOW( io.UINPUT_IOCTL_BASE, 100, io['int']); 6 | uinput.UI_SET_KEYBIT = io._IOW( io.UINPUT_IOCTL_BASE, 101, io['int']); 7 | uinput.UI_SET_RELBIT = io._IOW( io.UINPUT_IOCTL_BASE, 102, io['int']); 8 | uinput.UI_SET_ABSBIT = io._IOW( io.UINPUT_IOCTL_BASE, 103, io['int']); 9 | 10 | uinput.UI_DEV_CREATE = io._IO( io.UINPUT_IOCTL_BASE, 1 ); 11 | uinput.UI_DEV_DESTROY = io._IO( io.UINPUT_IOCTL_BASE, 2 ); 12 | 13 | uinput.EV_SYN = 0x00; 14 | uinput.EV_KEY = 0x01; 15 | uinput.EV_REL = 0x02; 16 | uinput.EV_ABS = 0x03; 17 | 18 | uinput.BTN_MOUSE = 0x110; 19 | uinput.BTN_LEFT = 0x110; 20 | uinput.BTN_RIGHT = 0x111; 21 | uinput.BTN_MIDDLE = 0x112; 22 | uinput.BTN_SIDE = 0x113; 23 | uinput.BTN_EXTRA = 0x114; 24 | uinput.BTN_FORWARD = 0x115; 25 | uinput.BTN_BACK = 0x116; 26 | uinput.BTN_TASK = 0x117; 27 | uinput.BTN_JOYSTICK = 0x120; 28 | uinput.BTN_TRIGGER = 0x120; 29 | uinput.BTN_THUMB = 0x121; 30 | uinput.BTN_THUMB2 = 0x122; 31 | uinput.BTN_TOP = 0x123; 32 | uinput.BTN_TOP2 = 0x124; 33 | uinput.BTN_PINKIE = 0x125; 34 | uinput.BTN_BASE = 0x126; 35 | uinput.BTN_BASE2 = 0x127; 36 | uinput.BTN_BASE3 = 0x128; 37 | uinput.BTN_BASE4 = 0x129; 38 | uinput.BTN_BASE5 = 0x12a; 39 | uinput.BTN_BASE6 = 0x12b; 40 | uinput.BTN_DEAD = 0x12f; 41 | uinput.BTN_GAMEPAD = 0x130; 42 | uinput.BTN_A = 0x130; 43 | uinput.BTN_B = 0x131; 44 | uinput.BTN_C = 0x132; 45 | uinput.BTN_X = 0x133; 46 | uinput.BTN_Y = 0x134; 47 | uinput.BTN_Z = 0x135; 48 | uinput.BTN_TL = 0x136; 49 | uinput.BTN_TR = 0x137; 50 | uinput.BTN_TL2 = 0x138; 51 | uinput.BTN_TR2 = 0x139; 52 | uinput.BTN_SELECT = 0x13a; 53 | uinput.BTN_START = 0x13b; 54 | uinput.BTN_MODE = 0x13c; 55 | uinput.BTN_THUMBL = 0x13d; 56 | uinput.BTN_THUMBR = 0x13e; 57 | uinput.REL_X = 0x00; 58 | uinput.REL_Y = 0x01; 59 | uinput.REL_WHEEL = 0x08; 60 | uinput.ABS_X = 0x00; 61 | uinput.ABS_Y = 0x01; 62 | uinput.ABS_Z = 0x02; 63 | 64 | uinput.ABS_RX = 0x03; 65 | uinput.ABS_RY = 0x04; 66 | uinput.ABS_RZ = 0x05; 67 | uinput.ABS_HAT0X = 0x10; 68 | uinput.ABS_HAT0Y = 0x11; 69 | uinput.ABS_MISC = 0x28; 70 | 71 | uinput.ABS_MAX = 0x3f; 72 | uinput.ABS_CNT = uinput.ABS_MAX + 1; 73 | 74 | uinput.ID_BUS = 0; 75 | uinput.BUS_USB = 0x3; 76 | 77 | uinput.UINPUT_MAX_NAME_SIZE = 80; 78 | 79 | for( var i in uinput ) { 80 | module.exports[ i ] = uinput[i]; 81 | } 82 | -------------------------------------------------------------------------------- /lib/io.js: -------------------------------------------------------------------------------- 1 | var io = {} 2 | 3 | io['int'] = 4; 4 | io['char'] = 1; 5 | io['char*'] = 8; 6 | 7 | io.UINPUT_IOCTL_BASE = 'U'.charCodeAt(); 8 | 9 | io._IOC_NONE = 0; 10 | io._IOC_WRITE = 1; 11 | io._IOC_READ = 2; 12 | 13 | io._IOC_NRBITS = 8; 14 | io._IOC_TYPEBITS = 8; 15 | 16 | io._IOC_SIZEBITS = 14; 17 | io._IOC_DIRBITS = 2; 18 | 19 | io._IOC_NRMASK = ((1 << io._IOC_NRBITS ) -1 ); 20 | io._IOC_TYPEMASK = ((1 << io._IOC_TYPEBITS ) -1 ); 21 | io._IOC_SIZEMASK = ((1 << io._IOC_SIZEBITS ) -1 ); 22 | io._IOC_DIRMASK = ((1 << io._IOC_DIRBITS ) -1 ); 23 | 24 | io._IOC_NRSHIFT = 0; 25 | io._IOC_TYPESHIFT = ( io._IOC_NRSHIFT + io._IOC_NRBITS ); 26 | io._IOC_SIZESHIFT = ( io._IOC_TYPESHIFT + io._IOC_TYPEBITS ); 27 | io._IOC_DIRSHIFT = ( io._IOC_SIZESHIFT + io._IOC_SIZEBITS ); 28 | 29 | 30 | 31 | io.sizeof = function(n){ 32 | switch ( typeof n ){ 33 | case 'number': 34 | return n; 35 | case 'string': 36 | return n.length; 37 | case 'object': 38 | return n.length ? n.length : 0; 39 | case 'undefined': 40 | return 0; 41 | } 42 | } 43 | 44 | 45 | 46 | io._IOC = function( dir, type, nr,size ) { 47 | return (( dir << io._IOC_DIRSHIFT ) | 48 | ( type << io._IOC_TYPESHIFT ) | 49 | ( nr << io._IOC_NRSHIFT ) | 50 | ( size << io._IOC_SIZESHIFT ) ); 51 | } 52 | 53 | 54 | io._IOC_TYPECHECK = function( t ) { 55 | return io.sizeof(t); 56 | } 57 | 58 | 59 | io._IO = function( type, nr ) { 60 | return io._IOC( io._IOC_NONE, type, nr, 0 ); 61 | } 62 | 63 | io._IOR = function( type, nr, size) { 64 | return io._IOC( io._IOC_READ, type, nr, io._IOC_TYPECHECK(size) ); 65 | } 66 | 67 | io._IOW = function( type, nr, size ) { 68 | return io._IOC( io._IOC_WRITE, type, nr, io._IOC_TYPECHECK(size)); 69 | } 70 | 71 | io._IOWR = function( type, nr, size ) { 72 | return io._IOC( io._IOC_READ | io._IOC_WRITE, type, nr, io._IOC_TYPECHECK(size)); 73 | } 74 | 75 | io._IOR_BAD = function( type, nr, size ) { 76 | return io._IOC( io._IOC_READ, type, nr, io.sizeof(size)); 77 | } 78 | 79 | io._IOW_BAD = function( type, nr, size ) { 80 | return io._IOC( io._IOC_WRITE, type, nr, io.sizeof(size)); 81 | } 82 | 83 | io._IOWR_BAD = function( type, nr, size ) { 84 | return io._IOC( io._IOC_READ | io._IOC_WRITE, type, nr, io.sizeof(size)); 85 | } 86 | 87 | 88 | 89 | /* used to decode ioctl numbers.. */ 90 | io._IOC_DIR = function( nr ) { 91 | return ( nr >> io._IOC_DIRSHIFT ) & io._IOC_DIRMASK; 92 | } 93 | 94 | io._IOC_TYPE = function( nr ) { 95 | return ( nr >> io._IOC_TYPESHIFT ) & io._IOC_TYPEMASK; 96 | } 97 | 98 | io._IOC_NR = function( nr ) { 99 | return ( nr >> io._IOC_NRSHIFT ) & io._IOC_NRMASK; 100 | } 101 | 102 | io._IOC_SIZE = function( nr ) { 103 | return ( nr >> io._IOC_SIZESHIFT ) & io._IOC_SIZEMASK; 104 | } 105 | 106 | 107 | module.exports = io; -------------------------------------------------------------------------------- /app/virtual_keyboard.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Created by roba91 on 15/08/2016 3 | Virtual keyboard class 4 | ### 5 | 6 | fs = require 'fs' 7 | ioctl = require 'ioctl' 8 | uinput = require '../lib/uinput' 9 | uinputStructs = require '../lib/uinput_structs' 10 | log = require '../lib/log' 11 | 12 | class virtual_keyboard 13 | 14 | constructor: () -> 15 | 16 | connect: (callback, error, retry=0) -> 17 | fs.open '/dev/uinput', 'w+', (err, fd) => 18 | if err 19 | log 'error', "Error on opening /dev/uinput:\n" + JSON.stringify(err) 20 | error err 21 | else 22 | @fd = fd 23 | 24 | # Init buttons 25 | ioctl @fd, uinput.UI_SET_EVBIT, uinput.EV_KEY 26 | for i in [0..255] 27 | ioctl @fd, uinput.UI_SET_KEYBIT, i 28 | 29 | uidev = new uinputStructs.uinput_user_dev 30 | uidev_buffer = uidev.ref() 31 | uidev_buffer.fill(0) 32 | uidev.name = Array.from("Virtual keyboard") 33 | uidev.id.bustype = uinput.BUS_USB 34 | uidev.id.vendor = 0x3 35 | uidev.id.product = 0x4 36 | uidev.id.version = 1 37 | 38 | fs.write @fd, uidev_buffer, 0, uidev_buffer.length, null, (err) => 39 | if err 40 | log 'error', "Error on init keyboard write:\n" + JSON.stringify(err) 41 | error err 42 | else 43 | try 44 | ioctl @fd, uinput.UI_DEV_CREATE 45 | callback() 46 | catch error 47 | log 'error', "Error on keyboard dev creation:\n" + JSON.stringify(err) 48 | fs.closeSync @fd 49 | @fd = undefined 50 | if retry < 5 51 | log 'info', "Retry to create keyboard" 52 | @connect callback, error, retry+1 53 | else 54 | log 'error', "Gave up on creating device" 55 | error err 56 | 57 | disconnect: (callback) -> 58 | if @fd 59 | ioctl @fd, uinput.UI_DEV_DESTROY 60 | fs.closeSync @fd 61 | @fd = undefined 62 | callback() 63 | 64 | sendEvent: (event) -> 65 | log 'debug', event 66 | if @fd 67 | ev = new uinputStructs.input_event 68 | ev.type = event.type 69 | ev.code = event.code 70 | ev.value = event.value 71 | ev.time.tv_sec = Math.round(Date.now() / 1000) 72 | ev.time.tv_usec = Math.round(Date.now() % 1000 * 1000) 73 | ev_buffer = ev.ref() 74 | 75 | ev_end = new uinputStructs.input_event 76 | ev_end.type = 0 77 | ev_end.code = 0 78 | ev_end.value = 0 79 | ev_end.time.tv_sec = Math.round(Date.now() / 1000) 80 | ev_end.time.tv_usec = Math.round(Date.now() % 1000 * 1000) 81 | ev_end_buffer = ev_end.ref() 82 | 83 | fs.writeSync @fd, ev_buffer, 0, ev_buffer.length, null 84 | fs.writeSync @fd, ev_end_buffer, 0, ev_end_buffer.length, null 85 | 86 | module.exports = virtual_keyboard 87 | -------------------------------------------------------------------------------- /public/css/touchpad.css: -------------------------------------------------------------------------------- 1 | body { 2 | -moz-user-select: none; 3 | -webkit-user-select: none; 4 | -ms-user-select: none; 5 | user-select: none; 6 | -o-user-select: none; 7 | } 8 | 9 | html, body { 10 | padding: 0; 11 | margin: 0; 12 | } 13 | 14 | body { 15 | overflow: hidden; 16 | position: fixed; 17 | top: 0; 18 | right: 0; 19 | bottom: 0; 20 | left: 0; 21 | } 22 | 23 | html { 24 | background: #292929 url(../images/noise.png); 25 | font-family: sans-serif; 26 | } 27 | 28 | button { 29 | font-size: 14px; 30 | background: #111; 31 | color: #666; 32 | border: 0; 33 | padding: 10px; 34 | margin: 7px; 35 | } 36 | 37 | button:hover { 38 | background: #050505; 39 | } 40 | 41 | button:active { 42 | color: #fff; 43 | background: #000; 44 | } 45 | 46 | #menu { 47 | background: #222; 48 | margin: 0; 49 | padding: 0; 50 | position: absolute; 51 | left: 0; 52 | right: 0; 53 | top: 0; 54 | bottom: 0; 55 | height: 50px; 56 | z-index: 10; 57 | } 58 | 59 | #touchpad { 60 | position: absolute; 61 | left: 0; 62 | right: 0; 63 | bottom: 0; 64 | top: 50px; 65 | } 66 | 67 | #touchpad-area { 68 | position: absolute; 69 | left: 0; 70 | top: 0; 71 | right: 0; 72 | bottom: 20%; 73 | background: -moz-radial-gradient(ellipse farthest-corner, rgba(255, 255, 255, 0.10) 0%, rgba(0, 0, 0, 0.01) 100%); /* FF3.6+ */ 74 | background: -webkit-radial-gradient(ellipse farthest-corner, rgba(255, 255, 255, 0.10) 0%, rgba(0, 0, 0, 0.01) 100%); /* Chrome10+,Safari5.1+ */ 75 | border-bottom: 1px solid #242424; 76 | } 77 | 78 | #touchpad-btn_left { 79 | left: 0; 80 | right: 50%; 81 | border-right: 1px solid #242424; 82 | } 83 | 84 | #touchpad-btn_right { 85 | left: 50%; 86 | right: 0; 87 | border-left: 1px solid #383838; 88 | } 89 | 90 | #touchpad-btn_left, 91 | #touchpad-btn_right { 92 | position: absolute; 93 | top: 80%; 94 | bottom: 0; 95 | border-top: 1px solid #383838; 96 | } 97 | 98 | #connecting { 99 | position: fixed; 100 | z-index: 100; 101 | left: 0; 102 | top: 0; 103 | bottom: 0; 104 | right: 0; 105 | background: rgba(0, 0, 0, 0.8); 106 | } 107 | 108 | @-webkit-keyframes blink { 109 | 0% { 110 | opacity: 1; 111 | } 112 | 50% { 113 | opacity: 0.4; 114 | } 115 | 100% { 116 | opacity: 1; 117 | } 118 | } 119 | 120 | @-moz-keyframes blink { 121 | 0% { 122 | opacity: 1; 123 | } 124 | 50% { 125 | opacity: 0.4; 126 | } 127 | 100% { 128 | opacity: 1; 129 | } 130 | } 131 | 132 | #connecting span { 133 | color: #fff; 134 | text-align: center; 135 | font-family: monospace; 136 | position: absolute; 137 | top: 50%; 138 | left: 0; 139 | right: 0; 140 | bottom: 50%; 141 | -moz-animation: blink 0.7s infinite; 142 | -webkit-animation: blink 0.7s infinite; 143 | } 144 | 145 | #settings-gear { 146 | float: right; 147 | height: 50px; 148 | } 149 | 150 | #settings-gear svg { 151 | fill: #666; 152 | margin-top: 7px; 153 | margin-right: 7px; 154 | height: 36px; 155 | width: 36px; 156 | cursor: pointer; 157 | } -------------------------------------------------------------------------------- /CREATE_KEYBOARD_LAYOUT.md: -------------------------------------------------------------------------------- 1 | Creating a new Keyboard Layout 2 | ============================== 3 | This document will first explain a the process of creating a new keyboard layout 4 | in a technical way. After that an example on how to do that with Inkscape 5 | (a free vector graphic editing software) is given. 6 | 7 | 8 | Technical View 9 | -------------- 10 | ### SVG Structure ### 11 | A keyboard layout is an svg file that contains one group that represents the 12 | whole keyboard. Inside that group you may place as many groups as wanted 13 | representing keys but no further groups. 14 | 15 | ### IDs of Key Groups ### 16 | Each key group must have an id that 17 | represents the keyCode (see below on how to get them). If the key is a modifier 18 | key (e.g. Ctrl) prepend 'm' in front of the keyCode (e.g. m100). Modifier keys 19 | are such keys that usually have no effect if pressed alone but only in 20 | combination with other keys. 21 | 22 | ### How to get the keyCodes ### 23 | If you're running a Linux system run `sudo showkey`. Then stroke the keys on 24 | your keyboard. 25 | 26 | ### Update the JavaScript ### 27 | Open public/js/keyboard/settings.js with the editor of you choice. Add your new 28 | keyboard layout to the `ALL_KEYBOARDS` object. Example if there was only the 29 | `us-US` layout and you added the `de-DE` layout, the file should change from 30 | 31 | var ALL_KEYBOARDS = { 32 | 'en-US': 'en-US.svg' 33 | }; 34 | 35 | to 36 | 37 | var ALL_KEYBOARDS = { 38 | 'en-US': 'en-US.svg', 39 | 'de-DE': 'de-DE.svg' 40 | }; 41 | 42 | where the key (left side of colon) is the name of the layout while the value 43 | (right side of colon) is the file name layout svg you just created under 44 | `public/images/keyboards`. HINT: don't forget the commas between the lines. 45 | 46 | With Inkscape 47 | ------------- 48 | Assumption: You have forked and cloned the repository. 49 | 50 | * Install Inkscape. Please check out their site 51 | [https://inkscape.org/](https://inkscape.org/) for more detail. 52 | * Go to `public/images/keyboards` under the project's path 53 | * Copy an existing keyboard close to the one you want to create. 54 | Or create a new svg file there. Please name it appropriately. 55 | * Open the file with Inkscape. 56 | * Modify the file as needed (don't use layers!) 57 | * Some tips for editing: 58 | * The whole keyboard is grouped. Ungroup it (Ctrl+Shift+G) 59 | * Tip: The font used is `Nimbus Sans L` 60 | * Convert all text to path (Select it and hit Ctrl+Shift+C) 61 | * Make sure each key is a group: 62 | * Select all parts of key and hit Ctrl+G 63 | * Go through the key groups from top left to top right (as you would read) 64 | * Select it (by clicking) 65 | * Press `End` key to lower the group to the bottom. 66 | This will bring the keys in a proper order an will make any 67 | debugging in the future much easier. 68 | * Open object properties (Ctrl+Shift+O) 69 | * Edit the id field according to the instructions above under 70 | ["Technical View" > "IDs of key groups"](#ids-of-key-groups) 71 | * Select the whole keyboard and group it (Ctrl+G again) 72 | * "save file as" (Ctrl+Shift+S) 73 | * select file type "Plain SVG" 74 | * overwrite your newly created file 75 | * Follow the instructions under 76 | ["Technical View" > "Update the JavaScript"](#update-the-javascript) 77 | * Test your layout! 78 | * Create a pull request =) 79 | * Thank you for your support! 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /server.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Created by MIROOF on 04/03/2015 3 | Virtual gamepad application 4 | ### 5 | 6 | path = require('path') 7 | express = require('express') 8 | app = express() 9 | http = require('http').Server(app) 10 | io = require('socket.io')(http) 11 | config = require './config.json' 12 | log = require './lib/log' 13 | 14 | gamepad_hub = require './app/virtual_gamepad_hub' 15 | gp_hub = new gamepad_hub() 16 | keyboard_hub = require './app/virtual_keyboard_hub' 17 | kb_hub = new keyboard_hub() 18 | touchpad_hub = require './app/virtual_touchpad_hub' 19 | tp_hub = new touchpad_hub() 20 | 21 | port = process.env.PORT || config.port 22 | 23 | # Add URL query string if analog mode is enabled 24 | if config.analog 25 | suffix = '?analog' 26 | else 27 | suffix = '' 28 | 29 | # draw routes 30 | app.get '/', (req, res) -> 31 | if config.useGamepadByDefault 32 | res.redirect 'gamepad.html' + suffix 33 | else 34 | res.redirect 'index.html' + suffix 35 | 36 | app.use(express.static(__dirname + '/public')); 37 | 38 | # socket io 39 | io.on 'connection', (socket) -> 40 | socket.on 'disconnect', () -> 41 | if socket.gamePadId != undefined 42 | log 'info', 'Gamepad disconnected' 43 | gp_hub.disconnectGamepad socket.gamePadId, () -> 44 | else if socket.keyBoardId != undefined 45 | log 'info', 'Keyboard disconnected' 46 | kb_hub.disconnectKeyboard socket.keyBoardId, () -> 47 | else if socket.touchpadId != undefined 48 | log 'info', 'Touchpad disconnected' 49 | tp_hub.disconnectTouchpad socket.touchpadId, () -> 50 | else 51 | log 'info', 'Unknown disconnect' 52 | 53 | socket.on 'connectGamepad', () -> 54 | gp_hub.connectGamepad (gamePadId) -> 55 | ledBitField = config.ledBitFieldSequence[gamePadId] 56 | if gamePadId != -1 57 | log 'info', 'connectGamepad: success' 58 | socket.gamePadId = gamePadId 59 | socket.emit 'gamepadConnected', {padId: gamePadId, ledBitField: ledBitField} 60 | else 61 | log 'warning', 'connectGamepad: failed' 62 | 63 | socket.on 'padEvent', (data) -> 64 | log 'debug', 'padEvent '+ JSON.stringify(data) 65 | if socket.gamePadId != undefined and data 66 | gp_hub.sendEvent socket.gamePadId, data 67 | 68 | 69 | socket.on 'connectKeyboard', () -> 70 | kb_hub.connectKeyboard (keyBoardId) -> 71 | if keyBoardId != -1 72 | log 'info', 'connectKeyboard: success' 73 | socket.keyBoardId = keyBoardId 74 | socket.emit 'keyboardConnected', {boardId: keyBoardId} 75 | else 76 | log 'info', 'connectKeyboard: failed' 77 | 78 | socket.on 'boardEvent', (data) -> 79 | log 'debug', 'boardEvent '+ JSON.stringify(data) 80 | if socket.keyBoardId != undefined and data 81 | kb_hub.sendEvent socket.keyBoardId, data 82 | 83 | socket.on 'connectTouchpad', () -> 84 | tp_hub.connectTouchpad (touchpadId) -> 85 | if touchpadId != -1 86 | log 'info', 'connectTouchpad: success' 87 | socket.touchpadId = touchpadId 88 | socket.emit 'touchpadConnected', {touchpadId: touchpadId} 89 | else 90 | log 'info', 'connectTouchpad: failed' 91 | 92 | socket.on 'touchpadEvent', (data) -> 93 | log 'debug', 'touchpadEvent '+ JSON.stringify(data) 94 | if socket.touchpadId != undefined and data 95 | tp_hub.sendEvent socket.touchpadId, data 96 | 97 | http.on 'error', (err) -> 98 | if err.hasOwnProperty('errno') 99 | switch err.errno 100 | when "EACCES" 101 | log 'error', "You don't have permissions to open port " + port + 102 | ". " + "For ports smaller than 1024, you need root privileges." 103 | throw err 104 | 105 | http.listen port, () -> 106 | log 'info', "Listening on #{port}" 107 | -------------------------------------------------------------------------------- /app/virtual_gamepad.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Created by MIROOF on 04/03/2015 3 | Virtual gamepad class 4 | ### 5 | 6 | fs = require 'fs' 7 | ioctl = require 'ioctl' 8 | uinput = require '../lib/uinput' 9 | uinputStructs = require '../lib/uinput_structs' 10 | log = require '../lib/log' 11 | 12 | 13 | class virtual_gamepad 14 | 15 | constructor: () -> 16 | 17 | connect: (callback, error, retry=0) -> 18 | fs.open '/dev/uinput', 'w+', (err, fd) => 19 | if err 20 | log 'error', "Error on opening /dev/uinput:\n" + JSON.stringify(err) 21 | error err 22 | else 23 | @fd = fd 24 | 25 | # Init buttons 26 | ioctl @fd, uinput.UI_SET_EVBIT, uinput.EV_KEY 27 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_A 28 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_B 29 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_X 30 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_Y 31 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_TL 32 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_TR 33 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_START 34 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_SELECT 35 | # Init directions 36 | ioctl @fd, uinput.UI_SET_EVBIT, uinput.EV_ABS 37 | ioctl @fd, uinput.UI_SET_ABSBIT, uinput.ABS_X 38 | ioctl @fd, uinput.UI_SET_ABSBIT, uinput.ABS_Y 39 | 40 | uidev = new uinputStructs.uinput_user_dev 41 | uidev_buffer = uidev.ref() 42 | uidev_buffer.fill(0) 43 | uidev.name = Array.from("Virtual gamepad") 44 | uidev.id.bustype = uinput.BUS_USB 45 | uidev.id.vendor = 0x3 46 | uidev.id.product = 0x3 47 | uidev.id.version = 2 48 | 49 | uidev.absmax[uinput.ABS_X] = 255 50 | uidev.absmin[uinput.ABS_X] = 0 51 | uidev.absfuzz[uinput.ABS_X] = 0 52 | uidev.absflat[uinput.ABS_X] = 15 53 | 54 | uidev.absmax[uinput.ABS_Y] = 255 55 | uidev.absmin[uinput.ABS_Y] = 0 56 | uidev.absfuzz[uinput.ABS_Y] = 0 57 | uidev.absflat[uinput.ABS_Y] = 15 58 | 59 | fs.write @fd, uidev_buffer, 0, uidev_buffer.length, null, (err) => 60 | if err 61 | log 'error', "Error on init gamepad write:\n" + JSON.stringify(err) 62 | error err 63 | else 64 | try 65 | ioctl @fd, uinput.UI_DEV_CREATE 66 | callback() 67 | catch err 68 | log 'error', "Error on gamepad dev creation:\n" + JSON.stringify(err) 69 | fs.closeSync @fd 70 | @fd = undefined 71 | if retry < 5 72 | log 'info', "Retry to create gamepad" 73 | @connect callback, error, retry+1 74 | else 75 | log 'error', "Gave up on creating device" 76 | error err 77 | 78 | disconnect: (callback) -> 79 | if @fd 80 | ioctl @fd, uinput.UI_DEV_DESTROY 81 | fs.closeSync @fd 82 | @fd = undefined 83 | callback() 84 | 85 | sendEvent: (event, error) -> 86 | if @fd 87 | ev = new uinputStructs.input_event 88 | ev.type = event.type 89 | ev.code = event.code 90 | ev.value = event.value 91 | ev.time.tv_sec = Math.round(Date.now() / 1000) 92 | ev.time.tv_usec = Math.round(Date.now() % 1000 * 1000) 93 | ev_buffer = ev.ref() 94 | 95 | ev_end = new uinputStructs.input_event 96 | ev_end.type = 0 97 | ev_end.code = 0 98 | ev_end.value = 0 99 | ev_end.time.tv_sec = Math.round(Date.now() / 1000) 100 | ev_end.time.tv_usec = Math.round(Date.now() % 1000 * 1000) 101 | ev_end_buffer = ev_end.ref() 102 | 103 | try 104 | fs.writeSync @fd, ev_buffer, 0, ev_buffer.length, null 105 | catch err 106 | log 'error', "Error on writing ev_buffer" 107 | throw err 108 | try 109 | fs.writeSync @fd, ev_end_buffer, 0, ev_end_buffer.length, null 110 | catch err 111 | log 'error', "Error on writing ev_end_buffer" 112 | throw err 113 | 114 | 115 | module.exports = virtual_gamepad 116 | -------------------------------------------------------------------------------- /app/virtual_keyboard.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | 3 | /* 4 | Created by roba91 on 15/08/2016 5 | Virtual keyboard class 6 | */ 7 | 8 | (function() { 9 | var fs, ioctl, log, uinput, uinputStructs, virtual_keyboard; 10 | 11 | fs = require('fs'); 12 | 13 | ioctl = require('ioctl'); 14 | 15 | uinput = require('../lib/uinput'); 16 | 17 | uinputStructs = require('../lib/uinput_structs'); 18 | 19 | log = require('../lib/log'); 20 | 21 | virtual_keyboard = (function() { 22 | function virtual_keyboard() {} 23 | 24 | virtual_keyboard.prototype.connect = function(callback, error, retry) { 25 | if (retry == null) { 26 | retry = 0; 27 | } 28 | return fs.open('/dev/uinput', 'w+', (function(_this) { 29 | return function(err, fd) { 30 | var i, j, uidev, uidev_buffer; 31 | if (err) { 32 | log('error', "Error on opening /dev/uinput:\n" + JSON.stringify(err)); 33 | return error(err); 34 | } else { 35 | _this.fd = fd; 36 | ioctl(_this.fd, uinput.UI_SET_EVBIT, uinput.EV_KEY); 37 | for (i = j = 0; j <= 255; i = ++j) { 38 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, i); 39 | } 40 | uidev = new uinputStructs.uinput_user_dev; 41 | uidev_buffer = uidev.ref(); 42 | uidev_buffer.fill(0); 43 | uidev.name = Array.from("Virtual keyboard"); 44 | uidev.id.bustype = uinput.BUS_USB; 45 | uidev.id.vendor = 0x3; 46 | uidev.id.product = 0x4; 47 | uidev.id.version = 1; 48 | return fs.write(_this.fd, uidev_buffer, 0, uidev_buffer.length, null, function(err) { 49 | var error1; 50 | if (err) { 51 | log('error', "Error on init keyboard write:\n" + JSON.stringify(err)); 52 | return error(err); 53 | } else { 54 | try { 55 | ioctl(_this.fd, uinput.UI_DEV_CREATE); 56 | return callback(); 57 | } catch (error1) { 58 | error = error1; 59 | log('error', "Error on keyboard dev creation:\n" + JSON.stringify(err)); 60 | fs.closeSync(_this.fd); 61 | _this.fd = void 0; 62 | if (retry < 5) { 63 | log('info', "Retry to create keyboard"); 64 | return _this.connect(callback, error, retry + 1); 65 | } else { 66 | log('error', "Gave up on creating device"); 67 | return error(err); 68 | } 69 | } 70 | } 71 | }); 72 | } 73 | }; 74 | })(this)); 75 | }; 76 | 77 | virtual_keyboard.prototype.disconnect = function(callback) { 78 | if (this.fd) { 79 | ioctl(this.fd, uinput.UI_DEV_DESTROY); 80 | fs.closeSync(this.fd); 81 | this.fd = void 0; 82 | return callback(); 83 | } 84 | }; 85 | 86 | virtual_keyboard.prototype.sendEvent = function(event) { 87 | var ev, ev_buffer, ev_end, ev_end_buffer; 88 | if (this.fd) { 89 | ev = new uinputStructs.input_event; 90 | ev.type = event.type; 91 | ev.code = event.code; 92 | ev.value = event.value; 93 | ev.time.tv_sec = Math.round(Date.now() / 1000); 94 | ev.time.tv_usec = Math.round(Date.now() % 1000 * 1000); 95 | ev_buffer = ev.ref(); 96 | ev_end = new uinputStructs.input_event; 97 | ev_end.type = 0; 98 | ev_end.code = 0; 99 | ev_end.value = 0; 100 | ev_end.time.tv_sec = Math.round(Date.now() / 1000); 101 | ev_end.time.tv_usec = Math.round(Date.now() % 1000 * 1000); 102 | ev_end_buffer = ev_end.ref(); 103 | fs.writeSync(this.fd, ev_buffer, 0, ev_buffer.length, null); 104 | return fs.writeSync(this.fd, ev_end_buffer, 0, ev_end_buffer.length, null); 105 | } 106 | }; 107 | 108 | return virtual_keyboard; 109 | 110 | })(); 111 | 112 | module.exports = virtual_keyboard; 113 | 114 | }).call(this); 115 | -------------------------------------------------------------------------------- /public/js/keyboard/input.js: -------------------------------------------------------------------------------- 1 | define(["jquery", "./utils", "./settings"], function ($, util, settings) { 2 | 3 | navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate; 4 | function hapticFeedback() { 5 | if (navigator.vibrate) { 6 | navigator.vibrate(50); 7 | } 8 | } 9 | 10 | var clickedKeys = []; 11 | var activeModKeys = {}; 12 | function bindClickAndTouchEvents(cb, settingsCb) { 13 | $("svg#keyboard > g > g#settings").on("mousedown touchstart", function () { 14 | $(this).attr('class', 'active'); 15 | hapticFeedback(); 16 | settingsCb(); 17 | $('#settings-modal').removeClass('closed'); 18 | }).on("mouseleave mouseup touchend", function (event) { 19 | $(this).removeAttr('class'); 20 | }); 21 | 22 | $(document).on("contextmenu",function(){ 23 | return false; 24 | }); 25 | 26 | // mousedown mouseup click touchstart touchend 27 | $("svg#keyboard > g > g:not(#settings)").on("mousedown mouseup click touchstart touchend", function (event) { 28 | if (event.cancelable) { 29 | event.preventDefault(); 30 | } 31 | }).on("touchmove", function (event) { 32 | if ($(this).data("left")) { 33 | return; 34 | } 35 | if (event.target !== document.elementFromPoint( 36 | event.originalEvent.targetTouches[0].pageX, 37 | event.originalEvent.targetTouches[0].pageY)) { 38 | $(this).trigger("mouseleave") 39 | .data("left", "true"); 40 | 41 | } 42 | }).on("touchend", function (event) { 43 | $(this).data("left", null); 44 | }).on("mousedown touchstart", function (event) { 45 | hapticFeedback(); 46 | 47 | $(this).attr('class', 'active'); 48 | var key = util.parseKeyId($(this).attr('id')); 49 | var keyIsModKey = key[0]; var keyCode = key[1]; 50 | 51 | if ($.inArray(keyCode, clickedKeys) === -1) { 52 | clickedKeys.push(keyCode); 53 | if (keyIsModKey && settings.stickyModKeys && !(keyCode in activeModKeys)) { 54 | activeModKeys[keyCode] = [0, $(this)]; 55 | console.info("Activated mod key", keyCode); 56 | if (cb != null) cb({type: 0x01, code: keyCode, value: 1, hardware: false}); 57 | } else { 58 | console.info("Clicked", keyCode); 59 | if (cb != null) cb({type: 0x01, code: keyCode, value: 1, hardware: false}); 60 | } 61 | } 62 | }).on("mouseleave mouseup touchend", function (event) { 63 | var key = util.parseKeyId($(this).attr('id')); 64 | var keyIsModKey = key[0]; var keyCode = key[1]; 65 | var idx = $.inArray(keyCode, clickedKeys); 66 | if (idx >= 0) { 67 | clickedKeys.splice(idx, 1); 68 | if (keyIsModKey && settings.stickyModKeys && keyCode in activeModKeys) { 69 | if (activeModKeys[keyCode][0] === 0) { 70 | // first key release 71 | activeModKeys[keyCode][0] = 1; 72 | return; 73 | } 74 | delete activeModKeys[keyCode]; 75 | $(this).removeAttr('class'); 76 | if (cb != null) cb({type: 0x01, code: keyCode, value: 0, hardware: false}); 77 | } else { 78 | console.info("Released click", keyCode); 79 | $(this).removeAttr('class'); 80 | if (cb != null) cb({type: 0x01, code: keyCode, value: 0, hardware: false}); 81 | for (key in activeModKeys) { 82 | var code = parseInt(key); 83 | activeModKeys[key][1].removeAttr('class'); 84 | delete activeModKeys[key]; 85 | console.info("Deactivated mod key", code); 86 | if (cb != null) cb({type: 0x01, code: code, value: 0, hardware: false}); 87 | } 88 | } 89 | } 90 | }); 91 | } 92 | 93 | return { 94 | listen: function (cb, settingsCb) { 95 | bindClickAndTouchEvents(cb, settingsCb); 96 | } 97 | } 98 | }); -------------------------------------------------------------------------------- /app/virtual_touchpad.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Virtual gamepad class 3 | ### 4 | 5 | fs = require 'fs' 6 | ioctl = require 'ioctl' 7 | uinput = require '../lib/uinput' 8 | uinputStructs = require '../lib/uinput_structs' 9 | log = require '../lib/log' 10 | 11 | class virtual_touchpad 12 | 13 | constructor: () -> 14 | 15 | connect: (callback, error, retry=0) -> 16 | fs.open '/dev/uinput', 'w+', (err, fd) => 17 | if err 18 | log 'error', "Error on opening /dev/uinput:\n" + JSON.stringify(err) 19 | error err 20 | else 21 | @fd = fd 22 | 23 | # Init buttons 24 | ioctl @fd, uinput.UI_SET_EVBIT, uinput.EV_KEY 25 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_LEFT 26 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_RIGHT 27 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_MIDDLE 28 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_A 29 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_B 30 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_X 31 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_Y 32 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_TL 33 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_TR 34 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_START 35 | ioctl @fd, uinput.UI_SET_KEYBIT, uinput.BTN_SELECT 36 | # Init absolute directions 37 | ioctl @fd, uinput.UI_SET_EVBIT, uinput.EV_ABS 38 | ioctl @fd, uinput.UI_SET_ABSBIT, uinput.ABS_X 39 | ioctl @fd, uinput.UI_SET_ABSBIT, uinput.ABS_Y 40 | # Init relative directions 41 | ioctl @fd, uinput.UI_SET_EVBIT, uinput.EV_REL 42 | ioctl @fd, uinput.UI_SET_RELBIT, uinput.REL_X 43 | ioctl @fd, uinput.UI_SET_RELBIT, uinput.REL_Y 44 | ioctl @fd, uinput.UI_SET_RELBIT, uinput.REL_WHEEL 45 | 46 | uidev = new uinputStructs.uinput_user_dev 47 | uidev_buffer = uidev.ref() 48 | uidev_buffer.fill(0) 49 | uidev.name = Array.from("Virtual touchpad") 50 | uidev.id.bustype = uinput.BUS_USB 51 | uidev.id.vendor = 0x3 52 | uidev.id.product = 0x5 53 | uidev.id.version = 1 54 | 55 | uidev.absmax[uinput.ABS_X] = 255 56 | uidev.absmin[uinput.ABS_X] = 0 57 | uidev.absfuzz[uinput.ABS_X] = 0 58 | uidev.absflat[uinput.ABS_X] = 15 59 | 60 | uidev.absmax[uinput.ABS_Y] = 255 61 | uidev.absmin[uinput.ABS_Y] = 0 62 | uidev.absfuzz[uinput.ABS_Y] = 0 63 | uidev.absflat[uinput.ABS_Y] = 15 64 | 65 | fs.write @fd, uidev_buffer, 0, uidev_buffer.length, null, (err) => 66 | if err 67 | log 'error', "Error on init touchpad write:\n" + JSON.stringify(err) 68 | error err 69 | else 70 | try 71 | ioctl @fd, uinput.UI_DEV_CREATE 72 | callback() 73 | catch err 74 | log 'error', "Error on touchpad create dev:\n" + JSON.stringify(err) 75 | fs.closeSync @fd 76 | @fd = undefined 77 | if retry < 5 78 | log 'info', "Retry to create touchpad" 79 | @connect callback, error, retry+1 80 | else 81 | log 'error', "Gave up on creating device" 82 | error err 83 | 84 | disconnect: (callback) -> 85 | if @fd 86 | ioctl @fd, uinput.UI_DEV_DESTROY 87 | fs.closeSync @fd 88 | @fd = undefined 89 | callback() 90 | 91 | sendEvent: (event) -> 92 | if @fd 93 | ev = new uinputStructs.input_event 94 | ev.type = event.type 95 | ev.code = event.code 96 | ev.value = event.value 97 | ev.time.tv_sec = Math.round(Date.now() / 1000) 98 | ev.time.tv_usec = Math.round(Date.now() % 1000 * 1000) 99 | ev_buffer = ev.ref() 100 | 101 | ev_end = new uinputStructs.input_event 102 | ev_end.type = 0 103 | ev_end.code = 0 104 | ev_end.value = 0 105 | ev_end.time.tv_sec = Math.round(Date.now() / 1000) 106 | ev_end.time.tv_usec = Math.round(Date.now() % 1000 * 1000) 107 | ev_end_buffer = ev_end.ref() 108 | 109 | try 110 | fs.writeSync @fd, ev_buffer, 0, ev_buffer.length, null 111 | catch err 112 | log 'error', "Error on writing ev_buffer" 113 | throw err 114 | try 115 | fs.writeSync @fd, ev_end_buffer, 0, ev_end_buffer.length, null 116 | catch err 117 | log 'error', "Error on writing ev_end_buffer" 118 | throw err 119 | 120 | 121 | module.exports = virtual_touchpad 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-virtual-gamepads 2 | 3 | This nodejs application turns your smartphone into a gamepad controller on Linux OS simply by reaching a local address. 4 | You can virtually plug in multiple gamepad controllers. 5 | 6 | Demo 7 | ---- 8 | Demo video 1 player in game [here](https://www.youtube.com/watch?v=OWgWugNsF7w) 9 | 10 | Demo video 3 players on EmulStation [here](https://www.youtube.com/watch?v=HQROnYLRyOw) 11 | 12 | Prerequisite 13 | ------------ 14 | This application is only compatible with Linux OS with the **uinput** kernel module installed. 15 | 16 | Installation 17 | ------------ 18 | git clone https://github.com/miroof/node-virtual-gamepads 19 | cd node-virtual-gamepads 20 | npm install 21 | 22 | If you encounter problems while installing or running node-virtual-gamepads have 23 | a look at the [troubleshooting](TROUBLESHOOTING.md) page. 24 | 25 | You can now configure the server to your needs. Just open `config.json` 26 | with the editor of you choice and adjust the values. Explanation of the 27 | individual values can be found in [README_CONFIG.md](README_CONFIG.md). 28 | 29 | To start the server run 30 | 31 | sudo node main.js 32 | 33 | Usage 34 | ----- 35 | Once the nodejs application is launched, you just have to plug your gamepad controller 36 | by connecting your device on the same local network and by reaching the address *http://node_server_address* 37 | 38 | Features 39 | -------- 40 | ### Plug up to 4 virtual gamepads 41 | The application will plug automatically a new controller when the web application is launched and unplug it at disconnection. 42 | 4 slots are available so 4 virtual gamepads can be created. You can see your current slot on the indicator directly on the vitual gamepad. 43 | 44 | ![Virtual gamepad](https://github.com/miroof/node-virtual-gamepads/blob/resources/screenshots/standalone.png?raw=true) 45 | 46 | ### Use it as standalone application (chrome mobile) 47 | With the [add to homescreen](https://developer.chrome.com/multidevice/android/installtohomescreen) chrome feature, 48 | you can easily use virtual gamepads application without launching the browser each time you want to play. 49 | 50 | With only 3 clicks, virtual gamepads web application becomes a standalone application. 51 | 52 | ![Standalone installation step 1](https://github.com/miroof/node-virtual-gamepads/blob/resources/screenshots/standalone_step1.png?raw=true) 53 | ![Standalone installation step 2](https://github.com/miroof/node-virtual-gamepads/blob/resources/screenshots/standalone_step2.png?raw=true) 54 | 55 | Then a shortcut is added on your homescreen and the application will be launched outside the browser. 56 | 57 | ![Virtual gamepad directly from the homescreen](https://github.com/miroof/node-virtual-gamepads/blob/resources/screenshots/standalone_step3.png?raw=true) 58 | ![Launched outside the browser](https://github.com/miroof/node-virtual-gamepads/blob/resources/screenshots/standalone_step4.png?raw=true) 59 | 60 | ### Enjoy haptic feedbacks 61 | Because it's difficult to spot the right place in a touch screen without looking at it, 62 | the touch zone of each button was increased. LT button was moved at the center of the screen 63 | to let as much space as possible for the joystick and avoid touch mistakes. 64 | 65 | ![Step 1](https://github.com/miroof/node-virtual-gamepads/blob/resources/schemas/touch_zones.png?raw=true) 66 | 67 | To know if we pressed a button with success, the web application provides an haptic feedback 68 | which can be easily deactivated by turning off the vibrations of the phone. 69 | 70 | ### Use the keyboard to enter text 71 | ![Virtual Keyboard](https://github.com/miroof/node-virtual-gamepads/blob/resources/screenshots/keyboard.png?raw=true) 72 | 73 | ### Use the touchpad for mouse inputs 74 | ![Virtual Touchpad](https://github.com/miroof/node-virtual-gamepads/blob/resources/screenshots/touchpad.png?raw=true) 75 | 76 | ### An index page lets you choose 77 | ![Index page](https://github.com/miroof/node-virtual-gamepads/blob/resources/screenshots/index.png?raw=true) 78 | 79 | Developing 80 | ---------- 81 | Please read the [contribution guideline](CONTRIBUTING.md) first if you haven't already. 82 | 83 | Clone this repository and install its dependencies with 84 | 85 | npm install 86 | 87 | When you change something in a coffeescript (e.g. main.coffee) run 88 | 89 | npx coffee -c main.coffee 90 | 91 | This will compile main.coffee to main.js which than can be run with node 92 | (see [Installation](README.md#installation)) 93 | To compile all coffee files when ever they change run 94 | 95 | npx coffee -cw . 96 | 97 | If you want do add a new keyboard layout please refer to [this file](CREATE_KEYBOARD_LAYOUT.md). 98 | -------------------------------------------------------------------------------- /public/js/lib/domReady.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license RequireJS domReady 2.0.1 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/requirejs/domReady for details 5 | */ 6 | /*jslint */ 7 | /*global require: false, define: false, requirejs: false, 8 | window: false, clearInterval: false, document: false, 9 | self: false, setInterval: false */ 10 | 11 | 12 | define(function () { 13 | 'use strict'; 14 | 15 | var isTop, testDiv, scrollIntervalId, 16 | isBrowser = typeof window !== "undefined" && window.document, 17 | isPageLoaded = !isBrowser, 18 | doc = isBrowser ? document : null, 19 | readyCalls = []; 20 | 21 | function runCallbacks(callbacks) { 22 | var i; 23 | for (i = 0; i < callbacks.length; i += 1) { 24 | callbacks[i](doc); 25 | } 26 | } 27 | 28 | function callReady() { 29 | var callbacks = readyCalls; 30 | 31 | if (isPageLoaded) { 32 | //Call the DOM ready callbacks 33 | if (callbacks.length) { 34 | readyCalls = []; 35 | runCallbacks(callbacks); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Sets the page as loaded. 42 | */ 43 | function pageLoaded() { 44 | if (!isPageLoaded) { 45 | isPageLoaded = true; 46 | if (scrollIntervalId) { 47 | clearInterval(scrollIntervalId); 48 | } 49 | 50 | callReady(); 51 | } 52 | } 53 | 54 | if (isBrowser) { 55 | if (document.addEventListener) { 56 | //Standards. Hooray! Assumption here that if standards based, 57 | //it knows about DOMContentLoaded. 58 | document.addEventListener("DOMContentLoaded", pageLoaded, false); 59 | window.addEventListener("load", pageLoaded, false); 60 | } else if (window.attachEvent) { 61 | window.attachEvent("onload", pageLoaded); 62 | 63 | testDiv = document.createElement('div'); 64 | try { 65 | isTop = window.frameElement === null; 66 | } catch (e) {} 67 | 68 | //DOMContentLoaded approximation that uses a doScroll, as found by 69 | //Diego Perini: http://javascript.nwbox.com/IEContentLoaded/, 70 | //but modified by other contributors, including jdalton 71 | if (testDiv.doScroll && isTop && window.external) { 72 | scrollIntervalId = setInterval(function () { 73 | try { 74 | testDiv.doScroll(); 75 | pageLoaded(); 76 | } catch (e) {} 77 | }, 30); 78 | } 79 | } 80 | 81 | //Check if document already complete, and if so, just trigger page load 82 | //listeners. Latest webkit browsers also use "interactive", and 83 | //will fire the onDOMContentLoaded before "interactive" but not after 84 | //entering "interactive" or "complete". More details: 85 | //http://dev.w3.org/html5/spec/the-end.html#the-end 86 | //http://stackoverflow.com/questions/3665561/document-readystate-of-interactive-vs-ondomcontentloaded 87 | //Hmm, this is more complicated on further use, see "firing too early" 88 | //bug: https://github.com/requirejs/domReady/issues/1 89 | //so removing the || document.readyState === "interactive" test. 90 | //There is still a window.onload binding that should get fired if 91 | //DOMContentLoaded is missed. 92 | if (document.readyState === "complete") { 93 | pageLoaded(); 94 | } 95 | } 96 | 97 | /** START OF PUBLIC API **/ 98 | 99 | /** 100 | * Registers a callback for DOM ready. If DOM is already ready, the 101 | * callback is called immediately. 102 | * @param {Function} callback 103 | */ 104 | function domReady(callback) { 105 | if (isPageLoaded) { 106 | callback(doc); 107 | } else { 108 | readyCalls.push(callback); 109 | } 110 | return domReady; 111 | } 112 | 113 | domReady.version = '2.0.1'; 114 | 115 | /** 116 | * Loader Plugin API method 117 | */ 118 | domReady.load = function (name, req, onLoad, config) { 119 | if (config.isBuild) { 120 | onLoad(null); 121 | } else { 122 | domReady(onLoad); 123 | } 124 | }; 125 | 126 | /** END OF PUBLIC API **/ 127 | 128 | return domReady; 129 | }); 130 | -------------------------------------------------------------------------------- /public/keyboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Virtual Keyboard 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
Loading...
30 | 31 | 84 | 85 |
86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | 3 | /* 4 | Created by MIROOF on 04/03/2015 5 | Virtual gamepad application 6 | */ 7 | 8 | (function() { 9 | var app, config, express, gamepad_hub, gp_hub, http, io, kb_hub, keyboard_hub, log, path, port, suffix, touchpad_hub, tp_hub; 10 | 11 | path = require('path'); 12 | 13 | express = require('express'); 14 | 15 | app = express(); 16 | 17 | http = require('http').Server(app); 18 | 19 | io = require('socket.io')(http); 20 | 21 | config = require('./config.json'); 22 | 23 | log = require('./lib/log'); 24 | 25 | gamepad_hub = require('./app/virtual_gamepad_hub'); 26 | 27 | gp_hub = new gamepad_hub(); 28 | 29 | keyboard_hub = require('./app/virtual_keyboard_hub'); 30 | 31 | kb_hub = new keyboard_hub(); 32 | 33 | touchpad_hub = require('./app/virtual_touchpad_hub'); 34 | 35 | tp_hub = new touchpad_hub(); 36 | 37 | port = process.env.PORT || config.port; 38 | 39 | if (config.analog) { 40 | suffix = '?analog'; 41 | } else { 42 | suffix = ''; 43 | } 44 | 45 | app.get('/', function(req, res) { 46 | if (config.useGamepadByDefault) { 47 | return res.redirect('gamepad.html' + suffix); 48 | } else { 49 | return res.redirect('index.html' + suffix); 50 | } 51 | }); 52 | 53 | app.use(express["static"](__dirname + '/public')); 54 | 55 | io.on('connection', function(socket) { 56 | socket.on('disconnect', function() { 57 | if (socket.gamePadId !== void 0) { 58 | log('info', 'Gamepad disconnected'); 59 | return gp_hub.disconnectGamepad(socket.gamePadId, function() {}); 60 | } else if (socket.keyBoardId !== void 0) { 61 | log('info', 'Keyboard disconnected'); 62 | return kb_hub.disconnectKeyboard(socket.keyBoardId, function() {}); 63 | } else if (socket.touchpadId !== void 0) { 64 | log('info', 'Touchpad disconnected'); 65 | return tp_hub.disconnectTouchpad(socket.touchpadId, function() {}); 66 | } else { 67 | return log('info', 'Unknown disconnect'); 68 | } 69 | }); 70 | socket.on('connectGamepad', function() { 71 | return gp_hub.connectGamepad(function(gamePadId) { 72 | var ledBitField; 73 | ledBitField = config.ledBitFieldSequence[gamePadId]; 74 | if (gamePadId !== -1) { 75 | log('info', 'connectGamepad: success'); 76 | socket.gamePadId = gamePadId; 77 | return socket.emit('gamepadConnected', { 78 | padId: gamePadId, 79 | ledBitField: ledBitField 80 | }); 81 | } else { 82 | return log('warning', 'connectGamepad: failed'); 83 | } 84 | }); 85 | }); 86 | socket.on('padEvent', function(data) { 87 | log('debug', 'padEvent ' + JSON.stringify(data)); 88 | if (socket.gamePadId !== void 0 && data) { 89 | return gp_hub.sendEvent(socket.gamePadId, data); 90 | } 91 | }); 92 | socket.on('connectKeyboard', function() { 93 | return kb_hub.connectKeyboard(function(keyBoardId) { 94 | if (keyBoardId !== -1) { 95 | log('info', 'connectKeyboard: success'); 96 | socket.keyBoardId = keyBoardId; 97 | return socket.emit('keyboardConnected', { 98 | boardId: keyBoardId 99 | }); 100 | } else { 101 | return log('info', 'connectKeyboard: failed'); 102 | } 103 | }); 104 | }); 105 | socket.on('boardEvent', function(data) { 106 | log('debug', 'boardEvent ' + JSON.stringify(data)); 107 | if (socket.keyBoardId !== void 0 && data) { 108 | return kb_hub.sendEvent(socket.keyBoardId, data); 109 | } 110 | }); 111 | socket.on('connectTouchpad', function() { 112 | return tp_hub.connectTouchpad(function(touchpadId) { 113 | if (touchpadId !== -1) { 114 | log('info', 'connectTouchpad: success'); 115 | socket.touchpadId = touchpadId; 116 | return socket.emit('touchpadConnected', { 117 | touchpadId: touchpadId 118 | }); 119 | } else { 120 | return log('info', 'connectTouchpad: failed'); 121 | } 122 | }); 123 | }); 124 | return socket.on('touchpadEvent', function(data) { 125 | log('debug', 'touchpadEvent ' + JSON.stringify(data)); 126 | if (socket.touchpadId !== void 0 && data) { 127 | return tp_hub.sendEvent(socket.touchpadId, data); 128 | } 129 | }); 130 | }); 131 | 132 | http.on('error', function(err) { 133 | if (err.hasOwnProperty('errno')) { 134 | switch (err.errno) { 135 | case "EACCES": 136 | log('error', "You don't have permissions to open port " + port + ". " + "For ports smaller than 1024, you need root privileges."); 137 | } 138 | } 139 | throw err; 140 | }); 141 | 142 | http.listen(port, function() { 143 | return log('info', "Listening on " + port); 144 | }); 145 | 146 | }).call(this); 147 | -------------------------------------------------------------------------------- /public/touchpad.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Virtual Touchpad 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 52 | 53 | 54 |
55 |
56 |
57 |
58 |
59 | 60 |
61 | connecting... 62 |
63 | 64 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /app/virtual_gamepad.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | 3 | /* 4 | Created by MIROOF on 04/03/2015 5 | Virtual gamepad class 6 | */ 7 | 8 | (function() { 9 | var fs, ioctl, log, uinput, uinputStructs, virtual_gamepad; 10 | 11 | fs = require('fs'); 12 | 13 | ioctl = require('ioctl'); 14 | 15 | uinput = require('../lib/uinput'); 16 | 17 | uinputStructs = require('../lib/uinput_structs'); 18 | 19 | log = require('../lib/log'); 20 | 21 | virtual_gamepad = (function() { 22 | function virtual_gamepad() {} 23 | 24 | virtual_gamepad.prototype.connect = function(callback, error, retry) { 25 | if (retry == null) { 26 | retry = 0; 27 | } 28 | return fs.open('/dev/uinput', 'w+', (function(_this) { 29 | return function(err, fd) { 30 | var uidev, uidev_buffer; 31 | if (err) { 32 | log('error', "Error on opening /dev/uinput:\n" + JSON.stringify(err)); 33 | return error(err); 34 | } else { 35 | _this.fd = fd; 36 | ioctl(_this.fd, uinput.UI_SET_EVBIT, uinput.EV_KEY); 37 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_A); 38 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_B); 39 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_X); 40 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_Y); 41 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_TL); 42 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_TR); 43 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_START); 44 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_SELECT); 45 | ioctl(_this.fd, uinput.UI_SET_EVBIT, uinput.EV_ABS); 46 | ioctl(_this.fd, uinput.UI_SET_ABSBIT, uinput.ABS_X); 47 | ioctl(_this.fd, uinput.UI_SET_ABSBIT, uinput.ABS_Y); 48 | uidev = new uinputStructs.uinput_user_dev; 49 | uidev_buffer = uidev.ref(); 50 | uidev_buffer.fill(0); 51 | uidev.name = Array.from("Virtual gamepad"); 52 | uidev.id.bustype = uinput.BUS_USB; 53 | uidev.id.vendor = 0x3; 54 | uidev.id.product = 0x3; 55 | uidev.id.version = 2; 56 | uidev.absmax[uinput.ABS_X] = 255; 57 | uidev.absmin[uinput.ABS_X] = 0; 58 | uidev.absfuzz[uinput.ABS_X] = 0; 59 | uidev.absflat[uinput.ABS_X] = 15; 60 | uidev.absmax[uinput.ABS_Y] = 255; 61 | uidev.absmin[uinput.ABS_Y] = 0; 62 | uidev.absfuzz[uinput.ABS_Y] = 0; 63 | uidev.absflat[uinput.ABS_Y] = 15; 64 | return fs.write(_this.fd, uidev_buffer, 0, uidev_buffer.length, null, function(err) { 65 | var error1; 66 | if (err) { 67 | log('error', "Error on init gamepad write:\n" + JSON.stringify(err)); 68 | return error(err); 69 | } else { 70 | try { 71 | ioctl(_this.fd, uinput.UI_DEV_CREATE); 72 | return callback(); 73 | } catch (error1) { 74 | err = error1; 75 | log('error', "Error on gamepad dev creation:\n" + JSON.stringify(err)); 76 | fs.closeSync(_this.fd); 77 | _this.fd = void 0; 78 | if (retry < 5) { 79 | log('info', "Retry to create gamepad"); 80 | return _this.connect(callback, error, retry + 1); 81 | } else { 82 | log('error', "Gave up on creating device"); 83 | return error(err); 84 | } 85 | } 86 | } 87 | }); 88 | } 89 | }; 90 | })(this)); 91 | }; 92 | 93 | virtual_gamepad.prototype.disconnect = function(callback) { 94 | if (this.fd) { 95 | ioctl(this.fd, uinput.UI_DEV_DESTROY); 96 | fs.closeSync(this.fd); 97 | this.fd = void 0; 98 | return callback(); 99 | } 100 | }; 101 | 102 | virtual_gamepad.prototype.sendEvent = function(event, error) { 103 | var err, error1, error2, ev, ev_buffer, ev_end, ev_end_buffer; 104 | if (this.fd) { 105 | ev = new uinputStructs.input_event; 106 | ev.type = event.type; 107 | ev.code = event.code; 108 | ev.value = event.value; 109 | ev.time.tv_sec = Math.round(Date.now() / 1000); 110 | ev.time.tv_usec = Math.round(Date.now() % 1000 * 1000); 111 | ev_buffer = ev.ref(); 112 | ev_end = new uinputStructs.input_event; 113 | ev_end.type = 0; 114 | ev_end.code = 0; 115 | ev_end.value = 0; 116 | ev_end.time.tv_sec = Math.round(Date.now() / 1000); 117 | ev_end.time.tv_usec = Math.round(Date.now() % 1000 * 1000); 118 | ev_end_buffer = ev_end.ref(); 119 | try { 120 | fs.writeSync(this.fd, ev_buffer, 0, ev_buffer.length, null); 121 | } catch (error1) { 122 | err = error1; 123 | log('error', "Error on writing ev_buffer"); 124 | throw err; 125 | } 126 | try { 127 | return fs.writeSync(this.fd, ev_end_buffer, 0, ev_end_buffer.length, null); 128 | } catch (error2) { 129 | err = error2; 130 | log('error', "Error on writing ev_end_buffer"); 131 | throw err; 132 | } 133 | } 134 | }; 135 | 136 | return virtual_gamepad; 137 | 138 | })(); 139 | 140 | module.exports = virtual_gamepad; 141 | 142 | }).call(this); 143 | -------------------------------------------------------------------------------- /public/js/keyboard/settings.js: -------------------------------------------------------------------------------- 1 | define(function () { 2 | 3 | var ALL_KEYBOARDS = { 4 | 'en-US': 'en-US.svg' 5 | }; 6 | 7 | var settings = {}; 8 | 9 | /* 10 | * Settings modal stuff 11 | */ 12 | 13 | settings.modal = {}; 14 | settings.modal.isOpen = false; 15 | 16 | // initialize settings modal 17 | require(["lib/domReady", "jquery"], function (domReady, $) { 18 | var settingsModal = $("#settings-modal"); 19 | 20 | settings.modal.open = function () { 21 | settingsModal.removeClass('closed'); 22 | settings.modal.isOpen = true; 23 | }; 24 | settings.modal.close = function () { 25 | settingsModal.addClass('closed'); 26 | settings.modal.isOpen = false; 27 | }; 28 | 29 | function initDialog() { 30 | for (var i in settings.ALL_KEYBOARDS) { 31 | if (settings.keyboardLayout == i) { 32 | $('#settings-layout').append( 33 | '' 34 | ); 35 | } else { 36 | $('#settings-layout').append( 37 | '' 38 | ); 39 | } 40 | } 41 | $('#settings-sticky').prop('checked', settings.stickyModKeys); 42 | } 43 | 44 | function bindSubmit() { 45 | $('#settings-form').submit(function (event) { 46 | var formData = {}; 47 | $(this).find(':input').each(function (i, e) { 48 | e = $(e); 49 | var name = e.attr('name'); 50 | if (name == null) return; 51 | var val; 52 | if (e.attr('type') == 'checkbox') { 53 | val = e.prop('checked'); 54 | } else { 55 | val = e.val(); 56 | } 57 | formData[name] = val; 58 | }); 59 | var oldKeyboardLayout = settings.keyboardLayout; 60 | settings.update(formData); 61 | settings.modal.close(); 62 | event.preventDefault(); 63 | event.stopPropagation(); 64 | if (oldKeyboardLayout != settings.keyboardLayout) { 65 | window.location.reload(); 66 | } 67 | }) 68 | } 69 | 70 | function bindClose() { 71 | settingsModal.find(".close").addBack().click(function (event) { 72 | settings.modal.close(); 73 | }); 74 | $(".modal").click(function (event) { 75 | event.stopPropagation(); 76 | }); 77 | } 78 | 79 | initDialog(); 80 | bindClose(); 81 | bindSubmit(); 82 | }); 83 | 84 | /* 85 | * Rest of the settings 86 | */ 87 | settings.KEYBOARDS_PATH = '/images/keyboards/'; 88 | settings.ALL_KEYBOARDS = ALL_KEYBOARDS; 89 | 90 | 91 | var localStorageAvailable = (typeof(Storage) !== "undefined"); 92 | 93 | settings.keyboardLayout = null; 94 | settings.stickyModKeys = null; 95 | 96 | settings.update = function(update) { 97 | if (update.hasOwnProperty('keyboardLayout')) settings.keyboardLayout = update.keyboardLayout; 98 | if (update.hasOwnProperty('stickyModKeys')) settings.stickyModKeys = update.stickyModKeys; 99 | if (localStorageAvailable) { 100 | window.localStorage.setItem('keyboardSettings', JSON.stringify({ 101 | keyboardLayout: settings.keyboardLayout, 102 | stickyModKeys: settings.stickyModKeys 103 | })); 104 | } 105 | }; 106 | 107 | function isTouchDevice() { 108 | try { 109 | document.createEvent("TouchEvent"); 110 | return true; 111 | } catch (e) { 112 | return false; 113 | } 114 | } 115 | 116 | function getBestLayoutMatch(lang) { 117 | if (lang in settings.ALL_KEYBOARDS) return lang; 118 | var lang_base = lang.split('-')[0]; 119 | if (lang_base in settings.ALL_KEYBOARDS) return lang_base; 120 | for (var kb in settings.ALL_KEYBOARDS) { 121 | if (lang_base == kb.split('-')[0]) return kb; 122 | } 123 | return Object.keys(settings.ALL_KEYBOARDS)[0]; 124 | } 125 | 126 | function defaultSettings() { 127 | var userLang = navigator.language || navigator.userLanguage; 128 | var keyboardLayout = getBestLayoutMatch(userLang); 129 | if (isTouchDevice()) { 130 | return { 131 | keyboardLayout: keyboardLayout, 132 | stickyModKeys: false, 133 | } 134 | } else { 135 | return { 136 | keyboardLayout: keyboardLayout, 137 | stickyModKeys: true, 138 | } 139 | } 140 | } 141 | 142 | function init() { 143 | if (localStorageAvailable) { 144 | var keyboardSettings = window.localStorage.getItem("keyboardSettings"); 145 | if (keyboardSettings == null) { 146 | keyboardSettings = defaultSettings(); 147 | } else { 148 | keyboardSettings = JSON.parse(keyboardSettings); 149 | } 150 | settings.update(keyboardSettings); 151 | } else { 152 | console.error('localStorage not available. Settings can\'t be stored.') 153 | } 154 | } 155 | 156 | init(); 157 | 158 | return settings; 159 | }); 160 | -------------------------------------------------------------------------------- /app/virtual_touchpad.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | 3 | /* 4 | Virtual gamepad class 5 | */ 6 | 7 | (function() { 8 | var fs, ioctl, log, uinput, uinputStructs, virtual_touchpad; 9 | 10 | fs = require('fs'); 11 | 12 | ioctl = require('ioctl'); 13 | 14 | uinput = require('../lib/uinput'); 15 | 16 | uinputStructs = require('../lib/uinput_structs'); 17 | 18 | log = require('../lib/log'); 19 | 20 | virtual_touchpad = (function() { 21 | function virtual_touchpad() {} 22 | 23 | virtual_touchpad.prototype.connect = function(callback, error, retry) { 24 | if (retry == null) { 25 | retry = 0; 26 | } 27 | return fs.open('/dev/uinput', 'w+', (function(_this) { 28 | return function(err, fd) { 29 | var uidev, uidev_buffer; 30 | if (err) { 31 | log('error', "Error on opening /dev/uinput:\n" + JSON.stringify(err)); 32 | return error(err); 33 | } else { 34 | _this.fd = fd; 35 | ioctl(_this.fd, uinput.UI_SET_EVBIT, uinput.EV_KEY); 36 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_LEFT); 37 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_RIGHT); 38 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_MIDDLE); 39 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_A); 40 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_B); 41 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_X); 42 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_Y); 43 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_TL); 44 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_TR); 45 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_START); 46 | ioctl(_this.fd, uinput.UI_SET_KEYBIT, uinput.BTN_SELECT); 47 | ioctl(_this.fd, uinput.UI_SET_EVBIT, uinput.EV_ABS); 48 | ioctl(_this.fd, uinput.UI_SET_ABSBIT, uinput.ABS_X); 49 | ioctl(_this.fd, uinput.UI_SET_ABSBIT, uinput.ABS_Y); 50 | ioctl(_this.fd, uinput.UI_SET_EVBIT, uinput.EV_REL); 51 | ioctl(_this.fd, uinput.UI_SET_RELBIT, uinput.REL_X); 52 | ioctl(_this.fd, uinput.UI_SET_RELBIT, uinput.REL_Y); 53 | ioctl(_this.fd, uinput.UI_SET_RELBIT, uinput.REL_WHEEL); 54 | uidev = new uinputStructs.uinput_user_dev; 55 | uidev_buffer = uidev.ref(); 56 | uidev_buffer.fill(0); 57 | uidev.name = Array.from("Virtual touchpad"); 58 | uidev.id.bustype = uinput.BUS_USB; 59 | uidev.id.vendor = 0x3; 60 | uidev.id.product = 0x5; 61 | uidev.id.version = 1; 62 | uidev.absmax[uinput.ABS_X] = 255; 63 | uidev.absmin[uinput.ABS_X] = 0; 64 | uidev.absfuzz[uinput.ABS_X] = 0; 65 | uidev.absflat[uinput.ABS_X] = 15; 66 | uidev.absmax[uinput.ABS_Y] = 255; 67 | uidev.absmin[uinput.ABS_Y] = 0; 68 | uidev.absfuzz[uinput.ABS_Y] = 0; 69 | uidev.absflat[uinput.ABS_Y] = 15; 70 | return fs.write(_this.fd, uidev_buffer, 0, uidev_buffer.length, null, function(err) { 71 | var error1; 72 | if (err) { 73 | log('error', "Error on init touchpad write:\n" + JSON.stringify(err)); 74 | return error(err); 75 | } else { 76 | try { 77 | ioctl(_this.fd, uinput.UI_DEV_CREATE); 78 | return callback(); 79 | } catch (error1) { 80 | err = error1; 81 | log('error', "Error on touchpad create dev:\n" + JSON.stringify(err)); 82 | fs.closeSync(_this.fd); 83 | _this.fd = void 0; 84 | if (retry < 5) { 85 | log('info', "Retry to create touchpad"); 86 | return _this.connect(callback, error, retry + 1); 87 | } else { 88 | log('error', "Gave up on creating device"); 89 | return error(err); 90 | } 91 | } 92 | } 93 | }); 94 | } 95 | }; 96 | })(this)); 97 | }; 98 | 99 | virtual_touchpad.prototype.disconnect = function(callback) { 100 | if (this.fd) { 101 | ioctl(this.fd, uinput.UI_DEV_DESTROY); 102 | fs.closeSync(this.fd); 103 | this.fd = void 0; 104 | return callback(); 105 | } 106 | }; 107 | 108 | virtual_touchpad.prototype.sendEvent = function(event) { 109 | var err, error1, error2, ev, ev_buffer, ev_end, ev_end_buffer; 110 | if (this.fd) { 111 | ev = new uinputStructs.input_event; 112 | ev.type = event.type; 113 | ev.code = event.code; 114 | ev.value = event.value; 115 | ev.time.tv_sec = Math.round(Date.now() / 1000); 116 | ev.time.tv_usec = Math.round(Date.now() % 1000 * 1000); 117 | ev_buffer = ev.ref(); 118 | ev_end = new uinputStructs.input_event; 119 | ev_end.type = 0; 120 | ev_end.code = 0; 121 | ev_end.value = 0; 122 | ev_end.time.tv_sec = Math.round(Date.now() / 1000); 123 | ev_end.time.tv_usec = Math.round(Date.now() % 1000 * 1000); 124 | ev_end_buffer = ev_end.ref(); 125 | try { 126 | fs.writeSync(this.fd, ev_buffer, 0, ev_buffer.length, null); 127 | } catch (error1) { 128 | err = error1; 129 | log('error', "Error on writing ev_buffer"); 130 | throw err; 131 | } 132 | try { 133 | return fs.writeSync(this.fd, ev_end_buffer, 0, ev_end_buffer.length, null); 134 | } catch (error2) { 135 | err = error2; 136 | log('error', "Error on writing ev_end_buffer"); 137 | throw err; 138 | } 139 | } 140 | }; 141 | 142 | return virtual_touchpad; 143 | 144 | })(); 145 | 146 | module.exports = virtual_touchpad; 147 | 148 | }).call(this); 149 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | -webkit-touch-callout: none; /* iOS Safari */ 3 | -webkit-user-select: none; /* Safari */ 4 | -khtml-user-select: none; /* Konqueror HTML */ 5 | -moz-user-select: none; /* Old versions of Firefox */ 6 | -ms-user-select: none; /* Internet Explorer/Edge */ 7 | user-select: none; /* Non-prefixed version, currently 8 | supported by Chrome, Opera and Firefox */ 9 | font-family: Verdana, Geneva, sans-serif; 10 | } 11 | 12 | td { 13 | text-align: center; 14 | } 15 | 16 | /********************\ 17 | |*** index styles ***| 18 | \********************/ 19 | body { 20 | transition: transform 0.3s ease-in, opacity 0.2s 0.1s ease-out; 21 | -webkit-transition: -webkit-transform 0.3s ease-in, opacity 0.2s 0.1s ease-out; 22 | -moz-transition: -moz-transform 0.3s ease-in, opacity 0.2s 0.1s ease-out; 23 | } 24 | 25 | body.slide-left { 26 | transform: translateX(-100%); 27 | -webkit-transform: translateX(-100%); 28 | -moz-transform: translateX(-100%); 29 | opacity: 0; 30 | } 31 | 32 | .control-selection:first-child { 33 | margin-top: 3em; 34 | } 35 | 36 | .control-selection { 37 | margin-top: 1em; 38 | width: 300px; 39 | margin-left: auto; 40 | margin-right: auto; 41 | border: solid #7c7c7c; 42 | border-radius: 5px; 43 | display: flex; 44 | flex-direction: row; 45 | align-items: center; 46 | padding: .5em; 47 | text-decoration: none; 48 | } 49 | 50 | .control-selection > img { 51 | display: inline-flex; 52 | height: 64px; 53 | width: 64px; 54 | } 55 | 56 | .control-selection > span.text { 57 | display: inline-flex; 58 | color: #242424; 59 | font-size: x-large; 60 | flex: 1 0; 61 | text-align: center; 62 | margin-left: 10px; 63 | } 64 | 65 | .control-selection > span.right-arrow { 66 | display: inline-flex; 67 | border: solid #7c7c7c; 68 | border-width: 0 5px 5px 0; 69 | padding: 10px; 70 | transform: scaleX(.5) rotate(-45deg); 71 | -webkit-transform: scaleX(.5) rotate(-45deg); 72 | -moz-transform: scaleX(.5) rotate(-45deg); 73 | } 74 | 75 | /**********************\ 76 | |*** gamepad styles ***| 77 | \**********************/ 78 | 79 | #dirContainer { 80 | position: absolute; 81 | top: 0px; 82 | left: 0px; 83 | height: 100%; 84 | width: 35%; 85 | } 86 | 87 | #slotIndicatorContainer { 88 | position: absolute; 89 | top: 0px; 90 | left: 0px; 91 | width: 100%; 92 | pointer-events: none; 93 | } 94 | 95 | #tableIndicator { 96 | width: 50px; 97 | margin: 0 auto; 98 | margin-top: 10%; 99 | } 100 | 101 | .indicator { 102 | width: 5px; 103 | height: 10px; 104 | background-color: black; 105 | margin: 2px; 106 | box-shadow: 1px 1px 1px 1px #aaa; 107 | } 108 | 109 | .indicatorSelected { 110 | background-color: red; 111 | } 112 | 113 | .btnSelected { 114 | stroke: black !important; 115 | stroke-width: 5 !important; 116 | } 117 | 118 | 119 | /***********************\ 120 | |*** keyboard styles ***| 121 | \***********************/ 122 | svg#keyboard { 123 | width: 100%; 124 | } 125 | 126 | svg#keyboard > g > g { 127 | cursor: pointer; 128 | } 129 | 130 | svg#keyboard > g > g.active > path:first-child { 131 | fill: rgb(255, 251, 175) !important; 132 | } 133 | 134 | small.explain { 135 | font-size: .7em; 136 | color: #888; 137 | line-height: 1em; 138 | } 139 | 140 | small.explain.center { 141 | width: 100%; 142 | text-align: center; 143 | display: inline-block; 144 | } 145 | 146 | a { 147 | color: #2196F3; 148 | } 149 | 150 | a:active { 151 | color: #1970b8; 152 | } 153 | 154 | /********************\ 155 | |*** form styling ***| 156 | \********************/ 157 | form .form-row { 158 | margin-top: 10px; 159 | margin-bottom: 10px; 160 | display: flex; 161 | line-height: 1.5em; 162 | font-size: 1.2em; 163 | } 164 | 165 | form .form-row > label { 166 | flex: 2; 167 | justify-content: flex-start; 168 | } 169 | 170 | form .form-row > input /*:not([type="checkbox"])*/, 171 | form .form-row > select, 172 | form .form-row > .nested { 173 | flex: 3; 174 | } 175 | 176 | form .form-row > .nested > select { 177 | padding: 0.3em; 178 | margin-bottom: 0.4em; 179 | } 180 | 181 | form .form-row > .nested > input { 182 | width: 100%; 183 | } 184 | 185 | @media (max-width: 550px) { 186 | form .form-row { 187 | margin-top: 20px; 188 | margin-bottom: 20px; 189 | flex-wrap: wrap; 190 | } 191 | 192 | form .form-row > * { 193 | line-height: 1.5em; 194 | font-size: 1.2em; 195 | } 196 | 197 | form .form-row > label { 198 | flex: initial; 199 | width: 100%; 200 | } 201 | 202 | form .form-row > input /*:not([type="checkbox"])*/, 203 | form .form-row > select, 204 | form .form-row > .nested { 205 | flex: initial; 206 | width: 100%; 207 | } 208 | } 209 | 210 | form button, 211 | form input[type="button"] { 212 | cursor: pointer; 213 | background: #c7c7c7; 214 | background-image: -webkit-linear-gradient(top, #c7c7c7, #7a7a7a); 215 | background-image: -moz-linear-gradient(top, #c7c7c7, #7a7a7a); 216 | background-image: -ms-linear-gradient(top, #c7c7c7, #7a7a7a); 217 | background-image: -o-linear-gradient(top, #c7c7c7, #7a7a7a); 218 | background-image: linear-gradient(to bottom, #c7c7c7, #7a7a7a); 219 | color: #ffffff; 220 | font-size: 20px; 221 | padding: 10px 20px 10px 20px; 222 | border: solid #7a7a7a 1px; 223 | text-decoration: none; 224 | } 225 | 226 | form button:hover, 227 | form input:hover[type="button"] { 228 | background: #7a7a7a; 229 | background-image: -webkit-linear-gradient(top, #7a7a7a, #c7c7c7); 230 | background-image: -moz-linear-gradient(top, #7a7a7a, #c7c7c7); 231 | background-image: -ms-linear-gradient(top, #7a7a7a, #c7c7c7); 232 | background-image: -o-linear-gradient(top, #7a7a7a, #c7c7c7); 233 | background-image: linear-gradient(to bottom, #7a7a7a, #c7c7c7); 234 | text-decoration: none; 235 | } 236 | 237 | /*********************\ 238 | |*** modal styling ***| 239 | \*********************/ 240 | .modal .credits h1, 241 | .modal .credits h2, 242 | .modal .credits h3, 243 | .modal .credits h4, 244 | .modal .credits h5, 245 | .modal .credits h6 { 246 | margin-top: 10px; 247 | margin-bottom: 0; 248 | } 249 | .modal .credits > :last-child { 250 | margin-bottom: 15px; 251 | display: block; 252 | } 253 | 254 | .modal { 255 | width: 500px; 256 | color: #bbb; 257 | background-color: #333; 258 | border: 4px solid #444; 259 | padding: 12px; 260 | overflow-y: auto; 261 | box-sizing: border-box; 262 | max-height: 100vh; 263 | } 264 | 265 | .modal > .close { 266 | cursor: pointer; 267 | float: right; 268 | position: relative; 269 | display: inline-block; 270 | width: 25px; 271 | height: 25px; 272 | overflow: hidden; 273 | } 274 | 275 | .modal > .close:after, .modal > .close:before { 276 | content: ''; 277 | position: absolute; 278 | width: 100%; 279 | top: 50%; 280 | left: 0; 281 | background: #f00; 282 | height: 6px; 283 | margin-top: -3px; 284 | } 285 | 286 | .modal > .close:before { 287 | transform: rotate(45deg); 288 | } 289 | 290 | .modal > .close:after { 291 | transform: rotate(-45deg); 292 | } 293 | 294 | .modal-wrapper.closed { 295 | top: 50%; 296 | bottom: 50%; 297 | left: 50%; 298 | right: 50%; 299 | overflow: hidden; 300 | } 301 | 302 | .modal-wrapper { 303 | /*display: none;*/ 304 | position: absolute; 305 | top: 0; 306 | bottom: 0; 307 | left: 0; 308 | right: 0; 309 | background-color: rgba(0, 0, 0, .3); 310 | z-index: 9001; /* it's over 9000!!! */ 311 | display: flex; 312 | align-items: center; 313 | justify-content: center; 314 | transition: top .3s, left .3s, right .3s, bottom .3s; 315 | } 316 | 317 | @media (max-width: 550px) { 318 | .modal-wrapper { 319 | align-items: flex-end; 320 | } 321 | 322 | .modal { 323 | width: 100%; 324 | } 325 | } 326 | 327 | /**********************************************************************\ 328 | |*** loading spinner as of http://projects.lukehaas.me/css-loaders/ ***| 329 | \**********************************************************************/ 330 | 331 | .loader { 332 | margin: 60px auto; 333 | font-size: 10px; 334 | position: relative; 335 | text-indent: -9999em; 336 | border: 1.1em solid rgba(71, 129, 255, 0.2); 337 | border-left-color: rgba(71, 129, 255, 1); 338 | -webkit-transform: translateZ(0); 339 | -ms-transform: translateZ(0); 340 | transform: translateZ(0); 341 | -webkit-animation: load8 1.1s infinite linear; 342 | animation: load8 1.1s infinite linear; 343 | } 344 | 345 | .loader, 346 | .loader:after { 347 | border-radius: 50%; 348 | width: 10em; 349 | height: 10em; 350 | } 351 | 352 | @-webkit-keyframes load8 { 353 | 0% { 354 | -webkit-transform: rotate(0deg); 355 | transform: rotate(0deg); 356 | } 357 | 100% { 358 | -webkit-transform: rotate(360deg); 359 | transform: rotate(360deg); 360 | } 361 | } 362 | 363 | @keyframes load8 { 364 | 0% { 365 | -webkit-transform: rotate(0deg); 366 | transform: rotate(0deg); 367 | } 368 | 100% { 369 | -webkit-transform: rotate(360deg); 370 | transform: rotate(360deg); 371 | } 372 | } 373 | 374 | /*************************************************************************\ 375 | |*** switch as of http://www.w3schools.com/howto/howto_css_switch.asp ***| 376 | \*************************************************************************/ 377 | 378 | /* The switch - the box around the slider */ 379 | .switch { 380 | position: relative; 381 | display: inline-block; 382 | width: 60px; 383 | height: 34px; 384 | } 385 | 386 | /* Hide default HTML checkbox */ 387 | .switch input { 388 | display: none; 389 | } 390 | 391 | /* The slider */ 392 | .slider { 393 | position: absolute; 394 | cursor: pointer; 395 | top: 0; 396 | left: 0; 397 | right: 0; 398 | bottom: 0; 399 | background-color: #ccc; 400 | -webkit-transition: .4s; 401 | transition: .4s; 402 | } 403 | 404 | .slider:before { 405 | position: absolute; 406 | content: ""; 407 | height: 26px; 408 | width: 26px; 409 | left: 4px; 410 | bottom: 4px; 411 | background-color: white; 412 | -webkit-transition: .4s; 413 | transition: .4s; 414 | } 415 | 416 | input:checked + .slider { 417 | background-color: #2196F3; 418 | } 419 | 420 | input:focus + .slider { 421 | box-shadow: 0 0 1px #2196F3; 422 | } 423 | 424 | input:checked + .slider:before { 425 | -webkit-transform: translateX(26px); 426 | -ms-transform: translateX(26px); 427 | transform: translateX(26px); 428 | } 429 | 430 | /* Rounded sliders */ 431 | .slider.round { 432 | border-radius: 34px; 433 | } 434 | 435 | .slider.round:before { 436 | border-radius: 50%; 437 | } 438 | -------------------------------------------------------------------------------- /public/js/lib/require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 2.2.0 Copyright jQuery Foundation and other contributors. 3 | Released under MIT license, http://github.com/requirejs/requirejs/LICENSE 4 | */ 5 | var requirejs,require,define; 6 | (function(ga){function ka(b,c,d,g){return g||""}function K(b){return"[object Function]"===Q.call(b)}function L(b){return"[object Array]"===Q.call(b)}function y(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(K(k)){if(this.events.error&&this.map.isDefine||g.onError!== 18 | ha)try{h=l.execCb(c,k,b,h)}catch(d){a=d}else h=l.execCb(c,k,b,h);this.map.isDefine&&void 0===h&&((b=this.module)?h=b.exports:this.usingExports&&(h=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",A(this.error=a)}else h=k;this.exports=h;if(this.map.isDefine&&!this.ignore&&(v[c]=h,g.onResourceLoad)){var f=[];y(this.depMaps,function(a){f.push(a.normalizedMap||a)});g.onResourceLoad(l,this.map,f)}C(c); 19 | this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}},callPlugin:function(){var a=this.map,b=a.id,d=q(a.prefix);this.depMaps.push(d);w(d,"defined",z(this,function(h){var k,f,d=e(fa,this.map.id),M=this.map.name,r=this.map.parentMap?this.map.parentMap.name:null,m=l.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(h.normalize&&(M=h.normalize(M,function(a){return c(a,r,!0)})|| 20 | ""),f=q(a.prefix+"!"+M,this.map.parentMap),w(f,"defined",z(this,function(a){this.map.normalizedMap=f;this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),h=e(t,f.id)){this.depMaps.push(f);if(this.events.error)h.on("error",z(this,function(a){this.emit("error",a)}));h.enable()}}else d?(this.map.url=l.nameToUrl(d),this.load()):(k=z(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),k.error=z(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];D(t,function(a){0=== 21 | a.map.id.indexOf(b+"_unnormalized")&&C(a.map.id)});A(a)}),k.fromText=z(this,function(h,c){var d=a.name,f=q(d),M=S;c&&(h=c);M&&(S=!1);u(f);x(p.config,b)&&(p.config[d]=p.config[b]);try{g.exec(h)}catch(e){return A(F("fromtexteval","fromText eval for "+b+" failed: "+e,e,[b]))}M&&(S=!0);this.depMaps.push(f);l.completeLoad(d);m([d],k)}),h.load(a.name,m,k,p))}));l.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){Z[this.map.id]=this;this.enabling=this.enabled=!0;y(this.depMaps,z(this,function(a, 22 | b){var c,h;if("string"===typeof a){a=q(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=e(R,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;w(a,"defined",z(this,function(a){this.undefed||(this.defineDep(b,a),this.check())}));this.errback?w(a,"error",z(this,this.errback)):this.events.error&&w(a,"error",z(this,function(a){this.emit("error",a)}))}c=a.id;h=t[c];x(R,c)||!h||h.enabled||l.enable(a,this)}));D(this.pluginMaps,z(this,function(a){var b=e(t,a.id); 23 | b&&!b.enabled&&l.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){y(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};l={config:p,contextName:b,registry:t,defined:v,urlFetched:W,defQueue:G,defQueueMap:{},Module:da,makeModuleMap:q,nextTick:g.nextTick,onError:A,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");if("string"===typeof a.urlArgs){var b= 24 | a.urlArgs;a.urlArgs=function(a,c){return(-1===c.indexOf("?")?"?":"&")+b}}var c=p.shim,h={paths:!0,bundles:!0,config:!0,map:!0};D(a,function(a,b){h[b]?(p[b]||(p[b]={}),Y(p[b],a,!0,!0)):p[b]=a});a.bundles&&D(a.bundles,function(a,b){y(a,function(a){a!==b&&(fa[a]=b)})});a.shim&&(D(a.shim,function(a,b){L(a)&&(a={deps:a});!a.exports&&!a.init||a.exportsFn||(a.exportsFn=l.makeShimExports(a));c[b]=a}),p.shim=c);a.packages&&y(a.packages,function(a){var b;a="string"===typeof a?{name:a}:a;b=a.name;a.location&& 25 | (p.paths[b]=a.location);p.pkgs[b]=a.name+"/"+(a.main||"main").replace(na,"").replace(U,"")});D(t,function(a,b){a.inited||a.map.unnormalized||(a.map=q(b,null,!0))});(a.deps||a.callback)&&l.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ga,arguments));return b||a.exports&&ia(a.exports)}},makeRequire:function(a,n){function m(c,d,f){var e,r;n.enableBuildCallback&&d&&K(d)&&(d.__requireJsBuild=!0);if("string"===typeof c){if(K(d))return A(F("requireargs", 26 | "Invalid require call"),f);if(a&&x(R,c))return R[c](t[a.id]);if(g.get)return g.get(l,c,a,m);e=q(c,a,!1,!0);e=e.id;return x(v,e)?v[e]:A(F("notloaded",'Module name "'+e+'" has not been loaded yet for context: '+b+(a?"":". Use require([])")))}P();l.nextTick(function(){P();r=u(q(null,a));r.skipMap=n.skipMap;r.init(c,d,f,{enabled:!0});H()});return m}n=n||{};Y(m,{isBrowser:E,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];-1!==f&&("."!==g&&".."!==g||1e.attachEvent.toString().indexOf("[native code")||ca?(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)):(S=!0,e.attachEvent("onreadystatechange",b.onScriptLoad));e.src=d;if(m.onNodeCreated)m.onNodeCreated(e,m,c,d);P=e;H?C.insertBefore(e,H):C.appendChild(e);P=null;return e}if(ja)try{setTimeout(function(){}, 35 | 0),importScripts(d),b.completeLoad(c)}catch(q){b.onError(F("importscripts","importScripts failed for "+c+" at "+d,q,[c]))}};E&&!w.skipDataMain&&X(document.getElementsByTagName("script"),function(b){C||(C=b.parentNode);if(O=b.getAttribute("data-main"))return u=O,w.baseUrl||-1!==u.indexOf("!")||(I=u.split("/"),u=I.pop(),T=I.length?I.join("/")+"/":"./",w.baseUrl=T),u=u.replace(U,""),g.jsExtRegExp.test(u)&&(u=O),w.deps=w.deps?w.deps.concat(u):[u],!0});define=function(b,c,d){var e,g;"string"!==typeof b&& 36 | (d=c,c=b,b=null);L(c)||(d=c,c=null);!c&&K(d)&&(c=[],d.length&&(d.toString().replace(qa,ka).replace(ra,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));S&&(e=P||pa())&&(b||(b=e.getAttribute("data-requiremodule")),g=J[e.getAttribute("data-requirecontext")]);g?(g.defQueue.push([b,c,d]),g.defQueueMap[b]=!0):V.push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(w)}})(this); 37 | -------------------------------------------------------------------------------- /public/js/virtualjoystick.js: -------------------------------------------------------------------------------- 1 | var VirtualJoystick = function(opts) 2 | { 3 | opts = opts || {}; 4 | this._container = opts.container || document.body; 5 | this._strokeStyle = opts.strokeStyle || 'cyan'; 6 | this._stickEl = opts.stickElement || this._buildJoystickStick(); 7 | this._baseEl = opts.baseElement || this._buildJoystickBase(); 8 | this._mouseSupport = opts.mouseSupport !== undefined ? opts.mouseSupport : false; 9 | this._stationaryBase = opts.stationaryBase || false; 10 | this._baseX = this._stickX = opts.baseX || 0 11 | this._baseY = this._stickY = opts.baseY || 0 12 | this._limitStickTravel = opts.limitStickTravel || false 13 | this._stickRadius = opts.stickRadius !== undefined ? opts.stickRadius : 100 14 | this._useCssTransform = opts.useCssTransform !== undefined ? opts.useCssTransform : false 15 | 16 | //this._container.style.position = "relative" 17 | 18 | this._container.appendChild(this._baseEl) 19 | this._baseEl.style.position = "absolute" 20 | this._baseEl.style.display = "none" 21 | this._container.appendChild(this._stickEl) 22 | this._stickEl.style.position = "absolute" 23 | this._stickEl.style.display = "none" 24 | 25 | this._pressed = false; 26 | this._touchIdx = null; 27 | 28 | if(this._stationaryBase === true){ 29 | this._baseEl.style.display = ""; 30 | this._baseEl.style.left = (this._baseX - this._baseEl.width /2)+"px"; 31 | this._baseEl.style.top = (this._baseY - this._baseEl.height/2)+"px"; 32 | } 33 | 34 | this._transform = this._useCssTransform ? this._getTransformProperty() : false; 35 | this._has3d = this._check3D(); 36 | 37 | var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 38 | this._$onTouchStart = __bind(this._onTouchStart , this); 39 | this._$onTouchEnd = __bind(this._onTouchEnd , this); 40 | this._$onTouchMove = __bind(this._onTouchMove , this); 41 | this._container.addEventListener( 'touchstart' , this._$onTouchStart , false ); 42 | this._container.addEventListener( 'touchend' , this._$onTouchEnd , false ); 43 | this._container.addEventListener( 'touchmove' , this._$onTouchMove , false ); 44 | if( this._mouseSupport ){ 45 | this._$onMouseDown = __bind(this._onMouseDown , this); 46 | this._$onMouseUp = __bind(this._onMouseUp , this); 47 | this._$onMouseMove = __bind(this._onMouseMove , this); 48 | this._container.addEventListener( 'mousedown' , this._$onMouseDown , false ); 49 | this._container.addEventListener( 'mouseup' , this._$onMouseUp , false ); 50 | this._container.addEventListener( 'mousemove' , this._$onMouseMove , false ); 51 | } 52 | } 53 | 54 | VirtualJoystick.prototype.destroy = function() 55 | { 56 | this._container.removeChild(this._baseEl); 57 | this._container.removeChild(this._stickEl); 58 | 59 | this._container.removeEventListener( 'touchstart' , this._$onTouchStart , false ); 60 | this._container.removeEventListener( 'touchend' , this._$onTouchEnd , false ); 61 | this._container.removeEventListener( 'touchmove' , this._$onTouchMove , false ); 62 | if( this._mouseSupport ){ 63 | this._container.removeEventListener( 'mouseup' , this._$onMouseUp , false ); 64 | this._container.removeEventListener( 'mousedown' , this._$onMouseDown , false ); 65 | this._container.removeEventListener( 'mousemove' , this._$onMouseMove , false ); 66 | } 67 | } 68 | 69 | /** 70 | * @returns {Boolean} true if touchscreen is currently available, false otherwise 71 | */ 72 | VirtualJoystick.touchScreenAvailable = function() 73 | { 74 | return 'createTouch' in document ? true : false; 75 | } 76 | 77 | /** 78 | * microevents.js - https://github.com/jeromeetienne/microevent.js 79 | */ 80 | ;(function(destObj){ 81 | destObj.addEventListener = function(event, fct){ 82 | if(this._events === undefined) this._events = {}; 83 | this._events[event] = this._events[event] || []; 84 | this._events[event].push(fct); 85 | return fct; 86 | }; 87 | destObj.removeEventListener = function(event, fct){ 88 | if(this._events === undefined) this._events = {}; 89 | if( event in this._events === false ) return; 90 | this._events[event].splice(this._events[event].indexOf(fct), 1); 91 | }; 92 | destObj.dispatchEvent = function(event /* , args... */){ 93 | if(this._events === undefined) this._events = {}; 94 | if( this._events[event] === undefined ) return; 95 | var tmpArray = this._events[event].slice(); 96 | for(var i = 0; i < tmpArray.length; i++){ 97 | var result = tmpArray[i].apply(this, Array.prototype.slice.call(arguments, 1)) 98 | if( result !== undefined ) return result; 99 | } 100 | return undefined 101 | }; 102 | })(VirtualJoystick.prototype); 103 | 104 | ////////////////////////////////////////////////////////////////////////////////// 105 | // // 106 | ////////////////////////////////////////////////////////////////////////////////// 107 | 108 | VirtualJoystick.prototype.deltaX = function(){ return this._stickX - this._baseX; } 109 | VirtualJoystick.prototype.deltaY = function(){ return this._stickY - this._baseY; } 110 | 111 | VirtualJoystick.prototype.up = function(){ 112 | if( this._pressed === false ) return false; 113 | var deltaX = this.deltaX(); 114 | var deltaY = this.deltaY(); 115 | if( deltaY >= 0 ) return false; 116 | if( Math.abs(deltaX) > 2*Math.abs(deltaY) ) return false; 117 | return true; 118 | } 119 | VirtualJoystick.prototype.down = function(){ 120 | if( this._pressed === false ) return false; 121 | var deltaX = this.deltaX(); 122 | var deltaY = this.deltaY(); 123 | if( deltaY <= 0 ) return false; 124 | if( Math.abs(deltaX) > 2*Math.abs(deltaY) ) return false; 125 | return true; 126 | } 127 | VirtualJoystick.prototype.right = function(){ 128 | if( this._pressed === false ) return false; 129 | var deltaX = this.deltaX(); 130 | var deltaY = this.deltaY(); 131 | if( deltaX <= 0 ) return false; 132 | if( Math.abs(deltaY) > 2*Math.abs(deltaX) ) return false; 133 | return true; 134 | } 135 | VirtualJoystick.prototype.left = function(){ 136 | if( this._pressed === false ) return false; 137 | var deltaX = this.deltaX(); 138 | var deltaY = this.deltaY(); 139 | if( deltaX >= 0 ) return false; 140 | if( Math.abs(deltaY) > 2*Math.abs(deltaX) ) return false; 141 | return true; 142 | } 143 | 144 | ////////////////////////////////////////////////////////////////////////////////// 145 | // // 146 | ////////////////////////////////////////////////////////////////////////////////// 147 | 148 | VirtualJoystick.prototype._onUp = function() 149 | { 150 | this._pressed = false; 151 | this._stickEl.style.display = "none"; 152 | 153 | if(this._stationaryBase == false){ 154 | this._baseEl.style.display = "none"; 155 | 156 | this._baseX = this._baseY = 0; 157 | this._stickX = this._stickY = 0; 158 | } 159 | } 160 | 161 | VirtualJoystick.prototype._onDown = function(x, y) 162 | { 163 | this._pressed = true; 164 | if(this._stationaryBase == false){ 165 | this._baseX = x; 166 | this._baseY = y; 167 | this._baseEl.style.display = ""; 168 | this._move(this._baseEl.style, (this._baseX - this._baseEl.width /2), (this._baseY - this._baseEl.height/2)); 169 | } 170 | 171 | this._stickX = x; 172 | this._stickY = y; 173 | 174 | if(this._limitStickTravel === true){ 175 | var deltaX = this.deltaX(); 176 | var deltaY = this.deltaY(); 177 | var stickDistance = Math.sqrt( (deltaX * deltaX) + (deltaY * deltaY) ); 178 | if(stickDistance > this._stickRadius){ 179 | var stickNormalizedX = deltaX / stickDistance; 180 | var stickNormalizedY = deltaY / stickDistance; 181 | 182 | this._stickX = stickNormalizedX * this._stickRadius + this._baseX; 183 | this._stickY = stickNormalizedY * this._stickRadius + this._baseY; 184 | } 185 | } 186 | 187 | this._stickEl.style.display = ""; 188 | this._move(this._stickEl.style, (this._stickX - this._stickEl.width /2), (this._stickY - this._stickEl.height/2)); 189 | } 190 | 191 | VirtualJoystick.prototype._onMove = function(x, y) 192 | { 193 | if( this._pressed === true ){ 194 | this._stickX = x; 195 | this._stickY = y; 196 | 197 | if(this._limitStickTravel === true){ 198 | var deltaX = this.deltaX(); 199 | var deltaY = this.deltaY(); 200 | var stickDistance = Math.sqrt( (deltaX * deltaX) + (deltaY * deltaY) ); 201 | if(stickDistance > this._stickRadius){ 202 | var stickNormalizedX = deltaX / stickDistance; 203 | var stickNormalizedY = deltaY / stickDistance; 204 | 205 | this._stickX = stickNormalizedX * this._stickRadius + this._baseX; 206 | this._stickY = stickNormalizedY * this._stickRadius + this._baseY; 207 | } 208 | } 209 | 210 | this._move(this._stickEl.style, (this._stickX - this._stickEl.width /2), (this._stickY - this._stickEl.height/2)); 211 | } 212 | } 213 | 214 | 215 | ////////////////////////////////////////////////////////////////////////////////// 216 | // bind touch events (and mouse events for debug) // 217 | ////////////////////////////////////////////////////////////////////////////////// 218 | 219 | VirtualJoystick.prototype._onMouseUp = function(event) 220 | { 221 | return this._onUp(); 222 | } 223 | 224 | VirtualJoystick.prototype._onMouseDown = function(event) 225 | { 226 | event.preventDefault(); 227 | var x = event.clientX; 228 | var y = event.clientY; 229 | return this._onDown(x, y); 230 | } 231 | 232 | VirtualJoystick.prototype._onMouseMove = function(event) 233 | { 234 | var x = event.clientX; 235 | var y = event.clientY; 236 | return this._onMove(x, y); 237 | } 238 | 239 | ////////////////////////////////////////////////////////////////////////////////// 240 | // comment // 241 | ////////////////////////////////////////////////////////////////////////////////// 242 | 243 | VirtualJoystick.prototype._onTouchStart = function(event) 244 | { 245 | // if there is already a touch inprogress do nothing 246 | if( this._touchIdx !== null ) return; 247 | 248 | // notify event for validation 249 | var isValid = this.dispatchEvent('touchStartValidation', event); 250 | if( isValid === false ) return; 251 | 252 | // dispatch touchStart 253 | this.dispatchEvent('touchStart', event); 254 | 255 | event.preventDefault(); 256 | // get the first who changed 257 | var touch = event.changedTouches[0]; 258 | // set the touchIdx of this joystick 259 | this._touchIdx = touch.identifier; 260 | 261 | // forward the action 262 | var x = touch.pageX; 263 | var y = touch.pageY; 264 | return this._onDown(x, y) 265 | } 266 | 267 | VirtualJoystick.prototype._onTouchEnd = function(event) 268 | { 269 | // if there is no touch in progress, do nothing 270 | if( this._touchIdx === null ) return; 271 | 272 | // dispatch touchEnd 273 | this.dispatchEvent('touchEnd', event); 274 | 275 | // try to find our touch event 276 | var touchList = event.changedTouches; 277 | for(var i = 0; i < touchList.length && touchList[i].identifier !== this._touchIdx; i++); 278 | // if touch event isnt found, 279 | if( i === touchList.length) return; 280 | 281 | // reset touchIdx - mark it as no-touch-in-progress 282 | this._touchIdx = null; 283 | 284 | //?????? 285 | // no preventDefault to get click event on ios 286 | event.preventDefault(); 287 | 288 | return this._onUp() 289 | } 290 | 291 | VirtualJoystick.prototype._onTouchMove = function(event) 292 | { 293 | // if there is no touch in progress, do nothing 294 | if( this._touchIdx === null ) return; 295 | 296 | // try to find our touch event 297 | var touchList = event.changedTouches; 298 | for(var i = 0; i < touchList.length && touchList[i].identifier !== this._touchIdx; i++ ); 299 | // if touch event with the proper identifier isnt found, do nothing 300 | if( i === touchList.length) return; 301 | var touch = touchList[i]; 302 | 303 | event.preventDefault(); 304 | 305 | var x = touch.pageX; 306 | var y = touch.pageY; 307 | return this._onMove(x, y) 308 | } 309 | 310 | 311 | ////////////////////////////////////////////////////////////////////////////////// 312 | // build default stickEl and baseEl // 313 | ////////////////////////////////////////////////////////////////////////////////// 314 | 315 | /** 316 | * build the canvas for joystick base 317 | */ 318 | VirtualJoystick.prototype._buildJoystickBase = function() 319 | { 320 | var canvas = document.createElement( 'canvas' ); 321 | canvas.width = 126; 322 | canvas.height = 126; 323 | 324 | var ctx = canvas.getContext('2d'); 325 | ctx.beginPath(); 326 | ctx.strokeStyle = this._strokeStyle; 327 | ctx.lineWidth = 6; 328 | ctx.arc( canvas.width/2, canvas.width/2, 40, 0, Math.PI*2, true); 329 | ctx.stroke(); 330 | 331 | ctx.beginPath(); 332 | ctx.strokeStyle = this._strokeStyle; 333 | ctx.lineWidth = 2; 334 | ctx.arc( canvas.width/2, canvas.width/2, 60, 0, Math.PI*2, true); 335 | ctx.stroke(); 336 | 337 | return canvas; 338 | } 339 | 340 | /** 341 | * build the canvas for joystick stick 342 | */ 343 | VirtualJoystick.prototype._buildJoystickStick = function() 344 | { 345 | var canvas = document.createElement( 'canvas' ); 346 | canvas.width = 86; 347 | canvas.height = 86; 348 | var ctx = canvas.getContext('2d'); 349 | ctx.beginPath(); 350 | ctx.strokeStyle = this._strokeStyle; 351 | ctx.lineWidth = 6; 352 | ctx.arc( canvas.width/2, canvas.width/2, 40, 0, Math.PI*2, true); 353 | ctx.stroke(); 354 | return canvas; 355 | } 356 | 357 | ////////////////////////////////////////////////////////////////////////////////// 358 | // move using translate3d method with fallback to translate > 'top' and 'left' 359 | // modified from https://github.com/component/translate and dependents 360 | ////////////////////////////////////////////////////////////////////////////////// 361 | 362 | VirtualJoystick.prototype._move = function(style, x, y) 363 | { 364 | if (this._transform) { 365 | if (this._has3d) { 366 | style[this._transform] = 'translate3d(' + x + 'px,' + y + 'px, 0)'; 367 | } else { 368 | style[this._transform] = 'translate(' + x + 'px,' + y + 'px)'; 369 | } 370 | } else { 371 | style.left = x + 'px'; 372 | style.top = y + 'px'; 373 | } 374 | } 375 | 376 | VirtualJoystick.prototype._getTransformProperty = function() 377 | { 378 | var styles = [ 379 | 'webkitTransform', 380 | 'MozTransform', 381 | 'msTransform', 382 | 'OTransform', 383 | 'transform' 384 | ]; 385 | 386 | var el = document.createElement('p'); 387 | var style; 388 | 389 | for (var i = 0; i < styles.length; i++) { 390 | style = styles[i]; 391 | if (null != el.style[style]) { 392 | return style; 393 | } 394 | } 395 | } 396 | 397 | VirtualJoystick.prototype._check3D = function() 398 | { 399 | var prop = this._getTransformProperty(); 400 | // IE8<= doesn't have `getComputedStyle` 401 | if (!prop || !window.getComputedStyle) return module.exports = false; 402 | 403 | var map = { 404 | webkitTransform: '-webkit-transform', 405 | OTransform: '-o-transform', 406 | msTransform: '-ms-transform', 407 | MozTransform: '-moz-transform', 408 | transform: 'transform' 409 | }; 410 | 411 | // from: https://gist.github.com/lorenzopolidori/3794226 412 | var el = document.createElement('div'); 413 | el.style[prop] = 'translate3d(1px,1px,1px)'; 414 | document.body.insertBefore(el, null); 415 | var val = getComputedStyle(el).getPropertyValue(map[prop]); 416 | document.body.removeChild(el); 417 | var exports = null != val && val.length && 'none' != val; 418 | return exports; 419 | } -------------------------------------------------------------------------------- /public/js/virtual_touchpad_client.js: -------------------------------------------------------------------------------- 1 | var TOUCHPAD = 'touchpad'; 2 | 3 | var JOYSTICK = 'joystick'; 4 | 5 | var settings = function () { 6 | 7 | var settings = {}; 8 | 9 | /* 10 | * Settings modal stuff 11 | */ 12 | 13 | settings.modal = {}; 14 | settings.modal.isOpen = false; 15 | 16 | // initialize settings modal 17 | $(document).ready(function () { 18 | var settingsModal = $("#settings-modal"); 19 | 20 | $('#settings-speed').on('input', function () { 21 | $('#settings-speed-output').val($(this).val()); 22 | }); 23 | $('#settings-acceleration').on('input', function () { 24 | $('#settings-acceleration-output').val($(this).val()); 25 | }); 26 | 27 | settings.modal.open = function () { 28 | settingsModal.removeClass('closed'); 29 | settings.modal.isOpen = true; 30 | }; 31 | settings.modal.close = function () { 32 | settingsModal.addClass('closed'); 33 | settings.modal.isOpen = false; 34 | }; 35 | 36 | function initDialog() { 37 | $('#settings-speed').val(settings.speed); 38 | $('#settings-speed-output').val(settings.speed); 39 | $('#settings-acceleration').val(settings.acceleration); 40 | $('#settings-acceleration-output').val(settings.acceleration); 41 | } 42 | 43 | function bindSubmit() { 44 | $('#settings-form').submit(function (event) { 45 | var formData = {}; 46 | $(this).find(':input').each(function (i, e) { 47 | e = $(e); 48 | var name = e.attr('name'); 49 | if (name == null) return; 50 | var val; 51 | if (e.attr('type') == 'checkbox') { 52 | val = e.prop('checked'); 53 | } else { 54 | val = e.val(); 55 | } 56 | formData[name] = val; 57 | }); 58 | settings.update(formData); 59 | settings.modal.close(); 60 | event.preventDefault(); 61 | event.stopPropagation(); 62 | }) 63 | } 64 | 65 | function bindClose() { 66 | settingsModal.find(".close").addBack().click(function (event) { 67 | settings.modal.close(); 68 | }); 69 | $(".modal").click(function (event) { 70 | event.stopPropagation(); 71 | }); 72 | } 73 | 74 | initDialog(); 75 | bindClose(); 76 | bindSubmit(); 77 | }); 78 | 79 | /* 80 | * Rest of the settings 81 | */ 82 | 83 | var localStorageAvailable = (typeof(Storage) !== "undefined"); 84 | 85 | settings.speed = null; 86 | settings.acceleration = null; 87 | 88 | settings.update = function(update) { 89 | if (update.hasOwnProperty('speed')) settings.speed = parseFloat(update.speed); 90 | if (update.hasOwnProperty('acceleration')) settings.acceleration = parseFloat(update.acceleration); 91 | if (localStorageAvailable) { 92 | window.localStorage.setItem('touchpadSettings', JSON.stringify({ 93 | speed: settings.speed, 94 | acceleration: settings.acceleration 95 | })); 96 | } 97 | }; 98 | 99 | function defaultSettings() { 100 | return { 101 | speed: 2, 102 | acceleration: 1.5 103 | } 104 | } 105 | 106 | function init() { 107 | if (localStorageAvailable) { 108 | var touchpadSettings = window.localStorage.getItem("touchpadSettings"); 109 | if (touchpadSettings == null) { 110 | touchpadSettings = defaultSettings(); 111 | } else { 112 | touchpadSettings = JSON.parse(touchpadSettings); 113 | } 114 | settings.update(touchpadSettings); 115 | } else { 116 | console.error('localStorage not available. Settings can\'t be stored.') 117 | } 118 | } 119 | 120 | init(); 121 | 122 | return settings; 123 | }(); 124 | 125 | 126 | // disable context menu e.g. on long touches on android 127 | $(function() { 128 | $(window).on("contextmenu", function(event) { 129 | event.preventDefault(); 130 | event.stopPropagation(); 131 | return false; 132 | }); 133 | }); 134 | 135 | 136 | var app = { 137 | 138 | clicks: 0, 139 | 140 | drag: 0, 141 | 142 | touches: 0, 143 | 144 | toucheindex: 0, 145 | 146 | touchmove: 0, 147 | 148 | current_x: 0, 149 | 150 | current_y: 0, 151 | 152 | current_device: TOUCHPAD, 153 | 154 | socket: null, 155 | 156 | createJoystickClient: function (options) { 157 | var menu_height = document.querySelector('body menu').clientHeight; 158 | 159 | var stick = new Joystick.CircuralStick({ 160 | start: function (coords) { 161 | }, 162 | move: function (abs_coords, rel_coords) { 163 | app.emit(["touchpadEvent", 3 /*'EV_ABS'*/, 0 /*'ABS_X'*/, rel_coords.x], 164 | ["touchpadEvent", 3 /*'EV_ABS'*/, 1 /*'ABS_Y'*/, rel_coords.y]); 165 | }, 166 | end: function () { 167 | }, 168 | analog: true, 169 | axis_value: 0x7FFF, 170 | x: document.body.clientWidth / 4, 171 | y: document.body.clientHeight / 2 + menu_height, 172 | container: document.getElementById('joystick'), 173 | autohide: false, 174 | targeting: true, 175 | region: function () { 176 | return [0, menu_height, document.body.clientWidth / 2, document.body.clientHeight] 177 | } 178 | }); 179 | 180 | var buttons = new Joystick.Buttons({ 181 | x: document.body.clientWidth / 2 + document.body.clientWidth / 4, 182 | y: document.body.clientHeight / 2, 183 | container: document.getElementById('joystick'), 184 | 185 | down: function (btn) { 186 | var code; 187 | switch (btn) { 188 | case 'button_x' : 189 | code = 0x133; 190 | /*'BTN_X'*/ 191 | break; 192 | case 'button_y' : 193 | code = 0x134; 194 | /*'BTN_Y'*/ 195 | break; 196 | case 'button_a' : 197 | code = 0x130; 198 | /*'BTN_A'*/ 199 | break; 200 | case 'button_b' : 201 | code = 0x131; 202 | /*'BTN_B'*/ 203 | break; 204 | } 205 | if (code) { 206 | app.emit("touchpadEvent", 1 /*'EV_KEY'*/, code, 1); 207 | } 208 | }, 209 | 210 | up: function (btn) { 211 | var code; 212 | switch (btn) { 213 | case 'button_x' : 214 | code = 0x133; 215 | /*'BTN_X'*/ 216 | break; 217 | case 'button_y' : 218 | code = 0x134; 219 | /*'BTN_Y'*/ 220 | break; 221 | case 'button_a' : 222 | code = 0x130; 223 | /*'BTN_A'*/ 224 | break; 225 | case 'button_b' : 226 | code = 0x131; 227 | /*'BTN_B'*/ 228 | break; 229 | } 230 | if (code) { 231 | app.emit("touchpadEvent", 1 /*'EV_KEY'*/, code, 0); 232 | } 233 | }, 234 | 235 | region: function () { 236 | return [document.body.clientWidth / 2, menu_height, document.body.clientWidth / 2, document.body.clientHeight] 237 | } 238 | }); 239 | 240 | window.addEventListener('resize', function () { 241 | buttons.setPosition(document.body.clientWidth / 2 + document.body.clientWidth / 4, document.body.clientHeight / 2); 242 | }) 243 | }, 244 | 245 | createTouchpadClient: function (options) { 246 | options.btn_left && options.btn_left.addEventListener('touchstart', function () { 247 | if (app.drag == 0) { 248 | app.emit("touchpadEvent", 1 /*'EV_KEY'*/, 0x110 /*'BTN_LEFT'*/, 1); 249 | } 250 | app.clicks += 1; 251 | }); 252 | options.btn_left && options.btn_left.addEventListener('touchend', function () { 253 | if (app.drag == 0) { 254 | app.emit("touchpadEvent", 1 /*'EV_KEY'*/, 0x110 /*'BTN_LEFT'*/, 0); 255 | } 256 | app.clicks -= 1; 257 | }); 258 | 259 | options.btn_right && options.btn_right.addEventListener('touchstart', function () { 260 | app.emit("touchpadEvent", 1 /*'EV_KEY'*/, 0x111 /*'BTN_RIGHT'*/, 1); 261 | app.clicks += 2; 262 | }); 263 | options.btn_right && options.btn_right.addEventListener('touchend', function () { 264 | app.emit("touchpadEvent", 1 /*'EV_KEY'*/, 0x111 /*'BTN_RIGHT'*/, 0); 265 | app.clicks -= 2; 266 | }); 267 | 268 | options.area && options.area.addEventListener('touchstart', function (e) { 269 | e.preventDefault(); 270 | // do not count touches into the mouse button areas 271 | app.touches = e.touches.length - (app.clicks & 1) - ((app.clicks & 2) >> 1); 272 | app.touchindex = e.touches.length - 1; 273 | app.touchmove = 0; 274 | app.current_x = e.touches[app.touchindex].pageX; 275 | app.current_y = e.touches[app.touchindex].pageY; 276 | }); 277 | 278 | options.area && options.area.addEventListener('touchmove', function (e) { 279 | e.preventDefault(); 280 | if (app.touchindex + 1 > e.touches.length) { 281 | app.touchindex = e.touches.length - 1; 282 | } else { 283 | var x = e.touches[app.touchindex].pageX - app.current_x; 284 | var y = e.touches[app.touchindex].pageY - app.current_y; 285 | x = (x >= 0 ? 1.0 : -1.0) * Math.pow(Math.abs(settings.speed * x), settings.acceleration); 286 | y = (y >= 0 ? 1.0 : -1.0) * Math.pow(Math.abs(settings.speed * y), settings.acceleration); 287 | if (app.touches >= 3) { 288 | // drag and drop 289 | if (app.drag == 0 && (app.clicks & 1) == 0) { 290 | app.emit("touchpadEvent", 1 /*'EV_KEY'*/, 0x110 /*'BTN_LEFT'*/, 1); 291 | } 292 | app.drag = 1; 293 | app.emit( 294 | ["touchpadEvent", 2 /*'EV_REL'*/, 0 /*'REL_X'*/, x], 295 | ["touchpadEvent", 2 /*'EV_REL'*/, 1 /*'REL_Y'*/, y] 296 | ); 297 | } else if (app.touches == 2) { 298 | app.emit( 299 | ["touchpadEvent", 2 /*'EV_REL'*/, 8 /*'REL_WHEEL' */, -x], 300 | ["touchpadEvent", 2 /*'EV_REL'*/, 6 /*'REL_HWHEEL'*/, -y] 301 | ); 302 | } else { 303 | app.emit( 304 | ["touchpadEvent", 2 /*'EV_REL'*/, 0 /*'REL_X'*/, x], 305 | ["touchpadEvent", 2 /*'EV_REL'*/, 1 /*'REL_Y'*/, y] 306 | ); 307 | } 308 | } 309 | app.current_x = e.touches[app.touchindex].pageX; 310 | app.current_y = e.touches[app.touchindex].pageY; 311 | app.touchmove = 1; 312 | }); 313 | 314 | options.area && options.area.addEventListener('touchend', function (e) { 315 | e.preventDefault(); 316 | if (app.touchmove == 1) { 317 | // end of a touchmove 318 | if (app.drag == 1 && (app.clicks & 1) == 0) { 319 | // end a drag and drop move 320 | app.emit("touchpadEvent", 1 /*'EV_KEY'*/, 0x110 /*'BTN_LEFT'*/, 0); 321 | } 322 | app.drag = 0; 323 | } else if (app.clicks == 0) { 324 | // There are no clicks in the mouse button areas, 325 | // only in this case register a tap on the touchpad as a mouse click. 326 | if (app.touches == 1) { 327 | app.emit( 328 | ["touchpadEvent", 1 /*'EV_KEY'*/, 0x110 /*'BTN_LEFT'*/, 1], 329 | ["touchpadEvent", 1 /*'EV_KEY'*/, 0x110 /*'BTN_LEFT'*/, 0] 330 | ); 331 | } else if (app.touches == 2) { 332 | app.emit( 333 | ["touchpadEvent", 1 /*'EV_KEY'*/, 0x111 /*'BTN_RIGHT'*/, 1], 334 | ["touchpadEvent", 1 /*'EV_KEY'*/, 0x111 /*'BTN_RIGHT'*/, 0] 335 | ); 336 | } else if (app.touches == 3) { 337 | app.emit( 338 | ["touchpadEvent", 1 /*'EV_KEY'*/, 0x112 /*'BTN_MIDDLE'*/, 1], 339 | ["touchpadEvent", 1 /*'EV_KEY'*/, 0x112 /*'BTN_MIDDLE'*/, 0] 340 | ); 341 | } else if (app.touches >= 4) { 342 | app.emit( 343 | ["touchpadEvent", 1 /*'EV_KEY'*/, 0x113 /*'BTN_SIDE'*/, 3], 344 | ["touchpadEvent", 1 /*'EV_KEY'*/, 0x113 /*'BTN_SIDE'*/, 3] 345 | ); 346 | } 347 | } 348 | app.touches = 0; 349 | }); 350 | }, 351 | 352 | emit: function () { 353 | if (!(arguments[0] instanceof Array)) { 354 | app.emit.call(this, Array.prototype.slice.call(arguments)); 355 | return; 356 | } 357 | 358 | Array.prototype.slice.call(arguments).forEach(function (ev) { 359 | app.socket && app.socket.emit(ev[0], { 360 | type: ev[1], 361 | code: ev[2], 362 | value: ev[3] 363 | }); 364 | }) 365 | }, 366 | 367 | init: function () { 368 | app.createJoystickClient({ 369 | area: document.getElementById('touchpad-area') 370 | }); 371 | 372 | app.createTouchpadClient({ 373 | area: document.getElementById('touchpad-area'), 374 | btn_left: document.getElementById('touchpad-btn_left'), 375 | btn_right: document.getElementById('touchpad-btn_right') 376 | }); 377 | 378 | var touchpad_screen = document.getElementById('touchpad'), 379 | joystick_screen = document.getElementById('joystick'); 380 | 381 | document.getElementById('goFullscreen').addEventListener('click', function () { 382 | app.toggleFullScreen(); 383 | }); 384 | document.getElementById('goFullscreen').addEventListener('touchend', function () { 385 | app.toggleFullScreen(); 386 | }); 387 | 388 | document.getElementById('setTouchpad').addEventListener('click', function () { 389 | joystick_screen.style.display = 'none'; 390 | touchpad_screen.style.display = 'block'; 391 | app.current_device = TOUCHPAD; 392 | }); 393 | document.getElementById('setTouchpad').addEventListener('touchend', function () { 394 | joystick_screen.style.display = 'none'; 395 | touchpad_screen.style.display = 'block'; 396 | app.current_device = TOUCHPAD; 397 | }); 398 | 399 | document.getElementById('setJoystick').addEventListener('click', function () { 400 | joystick_screen.style.display = 'block'; 401 | touchpad_screen.style.display = 'none'; 402 | app.current_device = JOYSTICK; 403 | }); 404 | document.getElementById('setJoystick').addEventListener('touchend', function () { 405 | joystick_screen.style.display = 'block'; 406 | touchpad_screen.style.display = 'none'; 407 | app.current_device = JOYSTICK; 408 | }); 409 | document.getElementById('gear-svg').addEventListener('click', function () { 410 | settings.modal.open(); 411 | }); 412 | 413 | !function connect() { 414 | app.socket = io(); 415 | 416 | app.socket.on("touchpadConnected", function (data) { 417 | slotNumber = data.touchpadId; 418 | document.getElementById('connecting').style.display = 'none'; 419 | }); 420 | 421 | app.socket.on("connect", function () { 422 | app.socket.emit("connectTouchpad", null); 423 | document.getElementById('connecting').style.display = 'block'; 424 | }); 425 | 426 | app.socket.on("disconnect", function () { 427 | location.reload(); 428 | }); 429 | }(); 430 | }, 431 | 432 | // Code from https://developer.mozilla.org/en-US/docs/Web/Guide/DOM/Using_full_screen_mode 433 | // because I'm to lazy... 434 | toggleFullScreen: function () { 435 | if (!document.fullscreenElement && // alternative standard method 436 | !document.mozFullScreenElement && !document.webkitFullscreenElement) { // current working methods 437 | if (document.documentElement.requestFullscreen) { 438 | document.documentElement.requestFullscreen(); 439 | } else if (document.documentElement.mozRequestFullScreen) { 440 | document.documentElement.mozRequestFullScreen(); 441 | } else if (document.documentElement.webkitRequestFullscreen) { 442 | document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); 443 | } 444 | } else { 445 | if (document.cancelFullScreen) { 446 | document.cancelFullScreen(); 447 | } else if (document.mozCancelFullScreen) { 448 | document.mozCancelFullScreen(); 449 | } else if (document.webkitCancelFullScreen) { 450 | document.webkitCancelFullScreen(); 451 | } 452 | } 453 | } 454 | }; 455 | -------------------------------------------------------------------------------- /public/js/virtual_gamepad_client.js: -------------------------------------------------------------------------------- 1 | /*********************** 2 | INITIALIZE THE JOYSTICK 3 | **********************/ 4 | var localStorageAvailable = (typeof(Storage) !== "undefined"); 5 | 6 | var setDirection = function() {}; 7 | var initJoystick = function () { 8 | var dirCursor = document.getElementById("dirCenter"); 9 | var container = document.getElementById("dirContainer"); 10 | var joystickBoundLimit = document.getElementById("path3212"); 11 | 12 | var joystick = new VirtualJoystick({ 13 | mouseSupport: true, 14 | stationaryBase: true, 15 | baseX: $(dirCursor).position().left, 16 | baseY: $(dirCursor).position().top, 17 | limitStickTravel: true, 18 | stickRadius: 50, 19 | baseElement: $(dirCursor).clone()[0], 20 | container: container, 21 | strokeStyle: '#777f82' 22 | }); 23 | 24 | $(window).resize(function () { 25 | joystick._baseX = $(dirCursor).position().left; 26 | joystick._baseY = $(dirCursor).position().top; 27 | }); 28 | 29 | var lastDirection = "none"; 30 | var analog = location.href.match(/\?analog/); 31 | setInterval(function(){ 32 | var gamepad = controllers[Object.keys(controllers)[0]] || null; 33 | if (analog) { 34 | /************ 35 | JOYSTICK MODE 36 | ***********/ 37 | if (joystick.left() || joystick.right() || joystick.up() || joystick.down() 38 | || (gamepad != null && (gamepad.xAxis || gamepad.yAxis))) { 39 | lastDirection = "dir"; 40 | var xy = { 41 | x: Math.round(127*(joystick.deltaX()/50 + 1)), 42 | y: Math.round(127*(joystick.deltaY()/50 + 1)) 43 | }; 44 | if (gamepad != null && (gamepad.xAxis || gamepad.yAxis)) { 45 | if (gamepad.xAxis != null) xy.x = Math.round(255.9999 * (gamepad.xAxis + 1) / 2 - .5); 46 | if (gamepad.yAxis != null) xy.y = Math.round(255.9999 * (gamepad.yAxis + 1) / 2 - .5); 47 | } 48 | setDirection(xy); 49 | } else if (lastDirection !== "none"){ 50 | lastDirection = "none"; 51 | setDirection({x: 127, y: 127}); 52 | } 53 | } else { 54 | /************ 55 | DIRECTIONAL PAD MODE 56 | ***********/ 57 | 58 | if (gamepad && gamepad.dpadState && gamepad.dpadState !== "") { 59 | setDirection({direction: gamepad.dpadState}); 60 | } else { 61 | var direction = ""; 62 | if (joystick.left()) direction += "l"; 63 | if (joystick.up()) direction += "u"; 64 | if (joystick.right()) direction += "r"; 65 | if (joystick.down()) direction += "d"; 66 | setDirection({direction: direction}) 67 | } 68 | } 69 | }, 1/30 * 1000); 70 | }; 71 | 72 | /************************* 73 | INITIALIZE REBIND MODAL 74 | ************************/ 75 | var rebindModal; 76 | $(function () { 77 | var $rebindModal = $("#rebind-modal"); 78 | var $skipBtn = $rebindModal.find('.skipBtn'); 79 | var $content = $rebindModal.find('.content'); 80 | var $initContent = $rebindModal.find(".init-content"); 81 | var $calibContent = $rebindModal.find(".calibration-content"); 82 | var isOpen = false; 83 | var newButtonMap = {}; 84 | 85 | var states = [ 86 | {}, // init state 87 | {}, // calibration state 88 | {type: 'button', text: '

Press UP on the D-Pad', mapName: 'dpadUP'}, 89 | {type: 'button', text: '

Press RIGHT on the D-Pad', mapName: 'dpadRIGHT'}, 90 | {type: 'button', text: '

Press DOWN on the D-Pad', mapName: 'dpadDOWN'}, 91 | {type: 'button', text: '

Press LEFT on the D-Pad', mapName: 'dpadLEFT'}, 92 | {type: 'button', text: '

Press the X Button (top button)', mapName: 'btnX'}, 93 | {type: 'button', text: '

Press the A Button (right button)', mapName: 'btnA'}, 94 | {type: 'button', text: '

Press the B Button (bottom button)', mapName: 'btnB'}, 95 | {type: 'button', text: '

Press the Y Button (left button)', mapName: 'btnY'}, 96 | {type: 'button', text: '

Press the SELECT Button', mapName: 'btnSELECT'}, 97 | {type: 'button', text: '

Press the START Button', mapName: 'btnSTART'}, 98 | {type: 'button', text: '

Press the L Button (left shoulder button)', mapName: 'btnLT'}, 99 | {type: 'button', text: '

Press the R Button (right shoulder button)', mapName: 'btnRT'}, 100 | {type: 'axis', text: '

Move the analog stick right', mapName: 'x'}, 101 | {type: 'axis', text: '

Move the analog stick down', mapName: 'y'} 102 | ]; 103 | 104 | var state = 0; 105 | 106 | rebindModal = { 107 | open: function() { 108 | state = 0; 109 | $skipBtn.hide(); 110 | $calibContent.hide(); 111 | $initContent.show(); 112 | $rebindModal.removeClass('closed'); 113 | isOpen = true; 114 | }, 115 | close: function () { 116 | $rebindModal.addClass('closed'); 117 | isOpen = false; 118 | }, 119 | isOpen: function () { 120 | return isOpen; 121 | }, 122 | gamepadNewButtonPress: function(buttonId) { 123 | if (state < 2) return; 124 | if (states[state].type !== 'button') return; 125 | newButtonMap[buttonId] = states[state].mapName; 126 | advanceState(); 127 | }, 128 | gamepadNewAxisActivity: function (axisId, sign) { 129 | if (state < 2) return; 130 | if (states[state].type === 'axis') { 131 | if (sign > 0) { 132 | newButtonMap['axis' + axisId] = states[state].mapName; 133 | } else { 134 | newButtonMap['axis' + axisId] = '-' + states[state].mapName; 135 | } 136 | } else { 137 | if (!newButtonMap['axis' + axisId]) { 138 | newButtonMap['axis' + axisId] = [null, null]; 139 | } 140 | if (sign < 0) { 141 | newButtonMap['axis' + axisId][0] = states[state].mapName; 142 | } else { 143 | newButtonMap['axis' + axisId][1] = states[state].mapName; 144 | } 145 | } 146 | advanceState(); 147 | } 148 | }; 149 | 150 | function advanceState() { 151 | if (state === states.length - 1) { 152 | buttonMap = newButtonMap; 153 | if (localStorageAvailable) { 154 | window.localStorage.setItem('gamepadButtonMap', JSON.stringify(buttonMap)); 155 | } 156 | rebindModal.close(); 157 | return; 158 | } 159 | state++; 160 | switch (state) { 161 | case 1: 162 | // start new button binding 163 | newButtonMap = {}; 164 | $initContent.hide(); 165 | $calibContent.show(); 166 | break; 167 | case 2: 168 | $calibContent.hide(); 169 | $skipBtn.show(); 170 | // no break! 171 | default: 172 | $content.html(states[state].text); 173 | } 174 | } 175 | 176 | $rebindModal.find('.noBtn').click(function(e) { 177 | e.preventDefault(); 178 | rebindModal.close(); 179 | }); 180 | 181 | $rebindModal.find('.yesBtn').click(function(e) { 182 | e.preventDefault(); 183 | advanceState(); 184 | }); 185 | 186 | $rebindModal.find('.close').click(function(e) { 187 | e.preventDefault(); 188 | rebindModal.close(); 189 | }); 190 | 191 | $calibContent.find('button').click(function(e) { 192 | e.preventDefault(); 193 | freezeAxesRest(); 194 | advanceState(); 195 | }); 196 | 197 | $skipBtn.click(function (e) { 198 | e.preventDefault(); 199 | advanceState(); 200 | }) 201 | }); 202 | 203 | /************************* 204 | INITIALIZE GAMEPADS 205 | ************************/ 206 | 207 | var controllers = {}; 208 | 209 | var buttonMap = ( 210 | (localStorageAvailable 211 | && JSON.parse(window.localStorage.getItem('gamepadButtonMap') || "null")) 212 | || { 213 | 0: "btnX", 214 | 1: "btnA", 215 | 2: "btnB", 216 | 3: "btnY", 217 | 4: "btnLT", 218 | 5: "btnRT", 219 | 8: "btnSELECT", 220 | 9: "btnSTART", 221 | 12: "dpadUP", 222 | 13: "dpadRIGHT", 223 | 14: "dpadDOWN", 224 | 15: "dpadLEFT", 225 | "axis0": "y", 226 | "axis1": "-x" 227 | } 228 | ); 229 | 230 | var axesNormalizeStats = (localStorageAvailable && JSON.parse(window.localStorage.getItem('gamepadAxesNormalizeStats') || "null") || {}); 231 | function normalizeAxis(axisId, value) { 232 | var changed = false; 233 | if (!axesNormalizeStats[axisId]) { 234 | axesNormalizeStats[axisId] = {min: value, max: value, rest: value}; 235 | changed = true; 236 | } 237 | if (value < 0 && value < axesNormalizeStats[axisId].min ) { 238 | axesNormalizeStats[axisId].min = value; 239 | changed = true; 240 | } else if (value > 0 && value > axesNormalizeStats[axisId].max ) { 241 | axesNormalizeStats[axisId].max = value; 242 | changed = true; 243 | } 244 | if (changed && localStorageAvailable) { 245 | window.localStorage.setItem('gamepadAxesNormalizeStats', JSON.stringify(axesNormalizeStats)); 246 | } 247 | var stat = axesNormalizeStats[axisId]; 248 | if (stat.max !== stat.min) { 249 | var deadZoneMax = stat.rest + (stat.max - stat.rest) * 0.075; 250 | var deadZoneMin = stat.rest - (stat.rest - stat.min) * 0.075; 251 | if (deadZoneMax >= value && value >= deadZoneMin) { 252 | return 0 253 | } else if (value > deadZoneMax) { 254 | if (stat.max === stat.rest) return 1; 255 | return (value - deadZoneMax) / (stat.max - deadZoneMax) 256 | } else { 257 | if (stat.min === stat.rest) return -1; 258 | return (value - deadZoneMin) / (deadZoneMin - stat.min) 259 | } 260 | } else { 261 | return stat.max 262 | } 263 | } 264 | function freezeAxesRest() { 265 | var controller = controllers[Object.keys(controllers)[0]]; 266 | if (!controller) return; 267 | for (var axisIdx in axesNormalizeStats) { 268 | var axis = controller.axes[axisIdx]; 269 | if (axis == null) continue; 270 | axesNormalizeStats[axisIdx].rest = axis; 271 | } 272 | if (localStorageAvailable) { 273 | window.localStorage.setItem('gamepadAxesNormalizeStats', JSON.stringify(axesNormalizeStats)); 274 | } 275 | } 276 | 277 | var wasPressedRaw = {buttons: {}, axes: {}}; 278 | var wasPressedMapped = {}; 279 | 280 | function handleGamepadButton(controller, mapName, pressed) { 281 | if (mapName == null) return; 282 | if (pressed) { 283 | //check for dpad buttons 284 | switch (mapName) { 285 | case "dpadUP": 286 | controller.dpadState += "u"; 287 | break; 288 | case "dpadRIGHT": 289 | controller.dpadState += "r"; 290 | break; 291 | case "dpadDOWN": 292 | controller.dpadState += "d"; 293 | break; 294 | case "dpadLEFT": 295 | controller.dpadState += "l"; 296 | break; 297 | default: 298 | $("#" + mapName).trigger('touchstart'); 299 | break; 300 | } 301 | wasPressedMapped[mapName] = true; 302 | } else { 303 | if (wasPressedMapped[mapName]) { 304 | if (!mapName.startsWith("dpad")) { 305 | $("#" + mapName).trigger('touchend'); 306 | } 307 | wasPressedMapped[mapName] = false; 308 | } 309 | } 310 | } 311 | 312 | function updateStatus() { 313 | var j, i, val, mapped; 314 | scangamepads(); 315 | // loop through all controllers 316 | for (j in controllers) { 317 | var controller = controllers[j]; 318 | controller.dpadState = ""; 319 | // loop through each button 320 | for (i = 0; i < controller.buttons.length; i++) { 321 | mapped = buttonMap[i]; 322 | val = controller.buttons[i]; 323 | var pressed = val === 1; 324 | if (typeof (val) == "object") { 325 | pressed = val.pressed || val.pressed || val.touched; 326 | val = val.value; 327 | } 328 | if (rebindModal.isOpen()) { 329 | if(pressed) { 330 | if (!wasPressedRaw.buttons[i]) { 331 | rebindModal.gamepadNewButtonPress(i); 332 | } 333 | wasPressedRaw.buttons[i] = true; 334 | } 335 | else { 336 | wasPressedRaw.buttons[i] = false; 337 | } 338 | } else { 339 | handleGamepadButton(controller, mapped, pressed); 340 | } 341 | } 342 | 343 | for (i = 0; i < controller.axes.length; i++) { 344 | val = normalizeAxis(i, controller.axes[i]); 345 | if (rebindModal.isOpen()) { 346 | if (Math.abs(val) > .5) { 347 | if (!wasPressedRaw.axes[i]) { 348 | rebindModal.gamepadNewAxisActivity(i, Math.sign(val)) 349 | } 350 | wasPressedRaw.axes[i] = true; 351 | } else { 352 | wasPressedRaw.axes[i] = false; 353 | } 354 | } else { 355 | mapped = buttonMap["axis" + i]; 356 | if (mapped == null) continue; 357 | if (typeof mapped === "string") { 358 | if (mapped.startsWith('-')) val *= -1; 359 | controller[mapped.slice(mapped.length-1) + 'Axis'] = val; 360 | } else { 361 | handleGamepadButton(controller, mapped[0], val < -.5); 362 | handleGamepadButton(controller, mapped[1], val > .5); 363 | } 364 | } 365 | } 366 | } 367 | window.requestAnimationFrame(updateStatus); 368 | } 369 | 370 | 371 | function scangamepads() { 372 | var gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []); 373 | for (var i = 0; i < gamepads.length; i++) { 374 | if (gamepads[i]) { 375 | if (!(gamepads[i].index in controllers)) { 376 | addgamepad(gamepads[i]); 377 | } else { 378 | controllers[gamepads[i].index] = gamepads[i]; 379 | } 380 | } 381 | } 382 | } 383 | 384 | $(function() { 385 | window.addEventListener("gamepadconnected", connecthandler); 386 | window.addEventListener("gamepaddisconnected", disconnecthandler); 387 | }); 388 | 389 | var rebindModalOpened = false; 390 | 391 | function connecthandler(e) { 392 | addgamepad(e.gamepad); 393 | } 394 | 395 | function addgamepad(gamepad) { 396 | if (!rebindModalOpened) { 397 | rebindModal.open(); 398 | rebindModalOpened = true; 399 | } 400 | controllers[gamepad.index] = gamepad; 401 | window.requestAnimationFrame(updateStatus); 402 | } 403 | 404 | function disconnecthandler(e) { 405 | removegamepad(e.gamepad); 406 | } 407 | 408 | function removegamepad(gamepad) { 409 | delete controllers[gamepad.index]; 410 | } 411 | 412 | /************************* 413 | INITIALIZE SLOT INDICATOR 414 | ************************/ 415 | var b0001 = parseInt('0001', 2); 416 | var b0010 = parseInt('0010', 2); 417 | var b0100 = parseInt('0100', 2); 418 | var b1000 = parseInt('1000', 2); 419 | var indicatorOn; 420 | var slotNumber; 421 | var ledBitField; 422 | var initSlotIndicator = function () { 423 | indicatorOn = false; 424 | var slotAnimationLoop = function () { 425 | if (ledBitField != null) { 426 | $(".indicator").removeClass("indicatorSelected"); 427 | if (ledBitField & b0001) { $("#indicator_1").addClass("indicatorSelected"); } 428 | if (ledBitField & b0010) { $("#indicator_2").addClass("indicatorSelected"); } 429 | if (ledBitField & b0100) { $("#indicator_3").addClass("indicatorSelected"); } 430 | if (ledBitField & b1000) { $("#indicator_4").addClass("indicatorSelected"); } 431 | } else { 432 | if(indicatorOn) { 433 | $(".indicator").removeClass("indicatorSelected"); 434 | } else { 435 | $(".indicator").addClass("indicatorSelected"); 436 | } 437 | indicatorOn = !indicatorOn; 438 | setTimeout(slotAnimationLoop, 500); 439 | } 440 | }; 441 | slotAnimationLoop(); 442 | }; 443 | 444 | /********************** 445 | HAPTIC CALLBACK METHOD 446 | *********************/ 447 | navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate; 448 | var hapticCallback = function () { 449 | if (navigator.vibrate) { 450 | navigator.vibrate(50); 451 | } 452 | }; 453 | 454 | // disable context menu e.g. on long touches on android 455 | function disableContextMenu() { 456 | $(window).on("contextmenu", function(event) { 457 | event.preventDefault(); 458 | event.stopPropagation(); 459 | return false; 460 | }); 461 | } 462 | 463 | /**************** 464 | MAIN ENTRY POINT 465 | ***************/ 466 | $( window ).load(function() { 467 | initJoystick(); 468 | initSlotIndicator(); 469 | disableContextMenu(); 470 | 471 | var socket = io(); 472 | 473 | socket.on("gamepadConnected", function(data) { 474 | slotNumber = data.padId; 475 | ledBitField = data.ledBitField; 476 | 477 | $(".btn") 478 | .off("touchstart touchend") 479 | .on("touchstart", function() { 480 | var btnId = $(this).data("btn"); 481 | $("#"+btnId).attr("class", "btnSelected"); 482 | socket.emit("padEvent", {type: 0x01, code: $(this).data("code"), value: 1}); 483 | hapticCallback(); 484 | }) 485 | .on("touchend", function() { 486 | var btnId = $(this).data("btn"); 487 | $("#"+btnId).attr("class", ""); 488 | socket.emit("padEvent", {type: 0x01, code: $(this).data("code"), value: 0}); 489 | //hapticCallback(); 490 | }); 491 | 492 | setDirection = function(direction) { 493 | if (direction.direction != null) { 494 | direction = direction.direction; 495 | if (direction.includes('l')) { 496 | socket.emit("padEvent", {type: 0x03, code: 0x00, value: 0}); 497 | } else if (direction.includes('r')) { 498 | socket.emit("padEvent", {type: 0x03, code: 0x00, value: 255}); 499 | } else { 500 | socket.emit("padEvent", {type: 0x03, code: 0x00, value: 127}); 501 | } 502 | if (direction.includes('u')) { 503 | socket.emit("padEvent", {type: 0x03, code: 0x01, value: 0}); 504 | } else if (direction.includes('d')) { 505 | socket.emit("padEvent", {type: 0x03, code: 0x01, value: 255}); 506 | } else { 507 | socket.emit("padEvent", {type: 0x03, code: 0x01, value: 127}); 508 | } 509 | } else { 510 | socket.emit("padEvent", {type: 0x03, code: 0x00, value: direction.x}); 511 | socket.emit("padEvent", {type: 0x03, code: 0x01, value: direction.y}); 512 | } 513 | }; 514 | 515 | setDirection({direction: "none"}); 516 | 517 | }); 518 | 519 | socket.on("connect", function() { 520 | socket.emit("connectGamepad", null); 521 | }); 522 | 523 | socket.on("disconnect", function() { 524 | location.reload(); 525 | }); 526 | } ); 527 | --------------------------------------------------------------------------------