├── .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 | 
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 | 
53 | 
54 |
55 | Then a shortcut is added on your homescreen and the application will be launched outside the browser.
56 |
57 | 
58 | 
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 | 
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 | 
72 |
73 | ### Use the touchpad for mouse inputs
74 | 
75 |
76 | ### An index page lets you choose
77 | 
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 |
41 |
42 |
53 |
54 |
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 |
--------------------------------------------------------------------------------