├── gimmevents-mouse.gif ├── gimmevents-keyboard.gif ├── src ├── main.js ├── _priv.js ├── emitter.js ├── receiver.js └── base.js ├── srv ├── client-base.js ├── client-receiver.js ├── pair.js ├── client-emitter.js └── main.js ├── .eslintrc ├── dist ├── emitter.html └── receiver.html ├── utils └── run.js ├── rollup.config.js ├── package.json ├── LICENSE ├── .gitignore ├── .fancom └── README.md /gimmevents-mouse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michalbe/gimmevents/HEAD/gimmevents-mouse.gif -------------------------------------------------------------------------------- /gimmevents-keyboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michalbe/gimmevents/HEAD/gimmevents-keyboard.gif -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { Emitter } from './emitter'; 2 | import { Receiver } from './receiver'; 3 | 4 | export default { 5 | Emitter, 6 | Receiver 7 | }; 8 | -------------------------------------------------------------------------------- /srv/client-base.js: -------------------------------------------------------------------------------- 1 | class ClientBase { 2 | constructor({ connection, role, pair }) { 3 | this.connection = connection; 4 | this.role = role; 5 | this.pair = pair; 6 | } 7 | } 8 | 9 | module.exports = ClientBase; 10 | -------------------------------------------------------------------------------- /srv/client-receiver.js: -------------------------------------------------------------------------------- 1 | const ClientBase = require('./client-base'); 2 | 3 | class ClientReceiver extends ClientBase { 4 | constructor(options) { 5 | options.role = 'receiver'; 6 | super(options); 7 | } 8 | } 9 | 10 | module.exports = ClientReceiver; 11 | -------------------------------------------------------------------------------- /src/_priv.js: -------------------------------------------------------------------------------- 1 | const privates = [ 2 | 'connect', 'on_connect', 'setup_listeners', 'send', 'listeners', 'on_message', 3 | 'handle_message' 4 | ]; 5 | 6 | export const priv = privates.reduce((memo, item) => { 7 | memo[item] = Symbol(item); 8 | return memo; 9 | }, {}); 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "defaults", 4 | "parser": "babel-eslint", 5 | "env": { 6 | "browser": true, 7 | "es6": true 8 | }, 9 | "rules": { 10 | "comma-dangle": [1, "only-multiline"], 11 | "no-console": 0 12 | }, 13 | "globals": { 14 | "FB": false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /dist/emitter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Emitter - Gimmevents 4 | 5 | 6 | I'm the Emitter. Press any key. 7 | 8 | 18 | 19 | -------------------------------------------------------------------------------- /srv/pair.js: -------------------------------------------------------------------------------- 1 | const CLIENT_ROLES = ['emitter', 'receiver']; 2 | 3 | class Pair { 4 | constructor({ key }) { 5 | this.key = key; 6 | } 7 | 8 | assign_client(client) { 9 | if (!CLIENT_ROLES.includes(client.role)) { 10 | throw new Error(`${client.role} is not a proper role.`); 11 | return; 12 | } 13 | 14 | this[client.role] = client; 15 | client.pair = this; 16 | } 17 | 18 | get_client_by_role(role) { 19 | return this[role]; 20 | } 21 | 22 | }; 23 | 24 | module.exports = Pair; 25 | -------------------------------------------------------------------------------- /dist/receiver.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Receiver - Gimmevents 4 | 5 | 6 | Other client pressed: nothing yet. 7 | 8 | 20 | 21 | -------------------------------------------------------------------------------- /srv/client-emitter.js: -------------------------------------------------------------------------------- 1 | const ClientBase = require('./client-base'); 2 | 3 | class ClientEmitter extends ClientBase { 4 | constructor(options) { 5 | options.role = 'emitter'; 6 | super(options); 7 | 8 | this.connection.on('message', (msg) => { 9 | msg = JSON.parse(msg); 10 | if (msg.action === 'update') { 11 | const receiver = this.pair.get_client_by_role('receiver'); 12 | if (!receiver) { 13 | return; 14 | } 15 | 16 | receiver.connection.send(JSON.stringify(msg)); 17 | } 18 | }); 19 | } 20 | } 21 | 22 | module.exports = ClientEmitter; 23 | -------------------------------------------------------------------------------- /src/emitter.js: -------------------------------------------------------------------------------- 1 | import { priv } from './_priv'; 2 | import { Base } from './base'; 3 | 4 | export class Emitter extends Base { 5 | constructor(options = {}) { 6 | options.role = 'emitter'; 7 | super(options); 8 | } 9 | 10 | register(event) { 11 | document.addEventListener(event, (evt) => { 12 | const { 13 | keyCode, 14 | which, 15 | key, 16 | screenX, 17 | screenY, 18 | clientX, 19 | clientY 20 | } = evt; 21 | 22 | this[priv.send]({ 23 | action: 'update', 24 | event_name: event, 25 | event_data: { 26 | keyCode, 27 | which, 28 | key, 29 | screenX, 30 | screenY, 31 | clientX, 32 | clientY 33 | } 34 | }) 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/receiver.js: -------------------------------------------------------------------------------- 1 | import { priv } from './_priv'; 2 | import { Base } from './base'; 3 | 4 | export class Receiver extends Base { 5 | constructor(options = {}) { 6 | options.role = 'receiver'; 7 | super(options); 8 | 9 | this[priv.listeners] = []; 10 | } 11 | 12 | on(event, handler) { 13 | this[priv.listeners].push({ 14 | event_name: event, 15 | handler, 16 | }); 17 | } 18 | 19 | [priv.handle_message] (message) { 20 | this[priv.listeners].forEach((listener) => { 21 | if (listener.event_name === message.event_name) { 22 | listener.handler(message.event_data); 23 | } 24 | }) 25 | } 26 | 27 | [priv.setup_listeners] () { 28 | this[priv.on_message](this[priv.handle_message].bind(this)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /utils/run.js: -------------------------------------------------------------------------------- 1 | const nodemon = require('nodemon'); 2 | const spawn = require('child_process').spawn; 3 | 4 | 5 | nodemon({ 6 | script: 'srv/main.js', 7 | ext: 'js json', 8 | ignore: ["dist/*", "src/*"], 9 | }); 10 | 11 | nodemon.on('start', () => { 12 | console.log('Server has started'); 13 | }).on('quit', () => { 14 | console.log('Server has quit'); 15 | process.exit(); 16 | }).on('restart', (files) => { 17 | console.log('Server restarted due to: ', files); 18 | }); 19 | 20 | 21 | const cmd_process = spawn( 22 | 'node', [ 23 | 'node_modules/rollup/bin/rollup', 24 | '-c', '-w' 25 | ] 26 | ); 27 | 28 | cmd_process.stdout.on('data', data => { 29 | console.log(data.toString()); 30 | }); 31 | 32 | cmd_process.stderr.on('data', data => { 33 | console.log(data.toString()); 34 | }); 35 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import babel from 'rollup-plugin-babel'; 4 | import serve from 'rollup-plugin-serve' 5 | import livereload from 'rollup-plugin-livereload' 6 | 7 | export default { 8 | input: 'src/main.js', 9 | output: { 10 | name: 'Gimmevents', 11 | format: 'umd', 12 | file: 'dist/gimmevents.js' 13 | }, 14 | plugins: [ 15 | commonjs({ 16 | include: 'node_modules/**', 17 | }), 18 | resolve({ 19 | customResolveOptions: { 20 | moduleDirectory: 'node_modules' 21 | } 22 | }), 23 | babel({ 24 | exclude: 'node_modules/**' 25 | }), 26 | serve({ 27 | contentBase: ['dist'] 28 | }), 29 | livereload({ 30 | watch: ['dist'] 31 | }) 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gimmevents", 3 | "version": "0.0.1", 4 | "author": "Michał Budzyński ", 5 | "repository": "git@github.com:michalbe/gimmevents.git", 6 | "devDependencies": { 7 | "babel-core": "^6.26.0", 8 | "babel-eslint": "^8.2.1", 9 | "babel-plugin-external-helpers": "^6.22.0", 10 | "babel-preset-latest": "^6.24.1", 11 | "eslint": "^4.17.0", 12 | "eslint-config-defaults": "^9.0.0", 13 | "nodemon": "^1.14.12", 14 | "rollup": "^0.55.5", 15 | "rollup-plugin-babel": "^3.0.3", 16 | "rollup-plugin-commonjs": "^8.3.0", 17 | "rollup-plugin-livereload": "^0.6.0", 18 | "rollup-plugin-node-resolve": "^3.0.2", 19 | "rollup-plugin-serve": "^0.4.2", 20 | "rollup-watch": "^4.3.1" 21 | }, 22 | "scripts": { 23 | "build": "rollup -c", 24 | "dev": "node utils/run.js" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /srv/main.js: -------------------------------------------------------------------------------- 1 | const port = 40510; 2 | 3 | const WSServer = require('ws').Server; 4 | const Pair = require('./pair'); 5 | 6 | const server = new WSServer({ port }); 7 | const pairs = new Map(); 8 | 9 | const Clients = { 10 | 'emitter': require('./client-emitter'), 11 | 'receiver': require('./client-receiver') 12 | }; 13 | 14 | server.on('connection', (ws) => { 15 | ws.on('message', (message) => { 16 | message = JSON.parse(message); 17 | if (!pairs.has(message.key)) { 18 | const pair = new Pair({ 19 | key: message.key 20 | }); 21 | pairs.set(message.key, pair); 22 | } 23 | 24 | const pair = pairs.get(message.key); 25 | if (message.action === 'create') { 26 | if (pair.get_client_by_role(message.role)) { 27 | ws.send(JSON.stringify({ 28 | message: `This pair already have a ${message.role}` 29 | })); 30 | ws.terminate(); 31 | } else { 32 | const client = new Clients[message.role]({ 33 | connection: ws 34 | }); 35 | 36 | pair.assign_client(client); 37 | } 38 | } 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/base.js: -------------------------------------------------------------------------------- 1 | import { priv } from './_priv'; 2 | 3 | export class Base { 4 | constructor({ key, host, role }) { 5 | this.key = key; 6 | this.host = host; 7 | this.role = role; 8 | this[priv.connect](); 9 | } 10 | 11 | [priv.connect] () { 12 | this.socket_handle = new WebSocket(`ws://${host}`); 13 | this.socket_handle.onopen = this[priv.on_connect].bind(this); 14 | } 15 | 16 | [priv.on_connect] () { 17 | console.log('Connected ...'); 18 | this.is_connected = true; 19 | this[priv.send]({ action: 'create' }); 20 | this[priv.setup_listeners](); 21 | } 22 | 23 | [priv.send] ({ action, event_name = '', event_data = {} }) { 24 | this.socket_handle.send(JSON.stringify({ 25 | key: this.key, 26 | role: this.role, 27 | action, 28 | event_name, 29 | event_data 30 | })); 31 | } 32 | 33 | [priv.on_message] (handler) { 34 | if (typeof handler !== 'function') { 35 | return; 36 | } 37 | 38 | this.socket_handle.onmessage = (msg) => { 39 | const data = JSON.parse(msg.data); 40 | handler(data); 41 | }; 42 | } 43 | 44 | [priv.setup_listeners] () { 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michał Budzyński 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .DS_Store 61 | dist/*.js 62 | -------------------------------------------------------------------------------- /.fancom: -------------------------------------------------------------------------------- 1 | # ____ 2 | # / __/___ _____ _________ ____ ___ 3 | # / /_/ __ `/ __ \/ ___/ __ \/ __ `__ \ 4 | # / __/ /_/ / / / / /__/ /_/ / / / / / / 5 | # /_/ \__,_/_/ /_/\___/\____/_/ /_/ /_/ 6 | # 7 | # Fancy Commit Manager Config file 8 | # https://github.com/michalbe/fancom 9 | 10 | # Name of the repository, default: name of the current repo 11 | #reponame="reponame" 12 | 13 | # Do we want to push to the temporary branch? 14 | pushtotemp=0 15 | 16 | # After pushing to the repo, what branch needs to be compared? 17 | #comparewith="branch to compare" 18 | 19 | # Suffix added to the temporary created, developer's branch, default: 'temp' 20 | #devsuffix="temp" 21 | 22 | # Set to `1` to add random emoji file to the commit message, default: no 23 | emoji=1 24 | 25 | # Add all the changes made in the repo to the commit, default: no 26 | #addall=1 27 | 28 | # Prefix added to the commit message, default: [REPONAME] 29 | #prefix="Shit's on fire yo" 30 | 31 | # Add branch name to the prefix of the commit, default: no 32 | #branchincommit=1 33 | 34 | # Skip the prefix in the commit message, default: no 35 | noprefix=1 36 | 37 | # Open github compare page in the browser after succesfull push 38 | opencompare=0 39 | 40 | # Github username, default: username of the owner of the current repo 41 | #author="authorname" 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gimmevents 2 | --- 3 | by [@michalbe](http://github.com/michalbe) 4 | 5 | ## WHAT? 6 | [Gimmevents](https://github.com/michalbe/gimmevents) is a simple event tunnel that sends keyboard and mouse events from one browser to another. CLients needs to register with a unique key. 7 | 8 | ## WHY? 9 | I don't have any VR controller what makes my [a-frame](https://github.com/aframevr/aframe) based games boring and non-interactive. With [Gimmevents](https://github.com/michalbe/gimmevents) I can render game on my VR set (for now it's just my phone) and still control it with my computer's keyboard or mouse. 10 | 11 | ## HOW? 12 | [Gimmevents](https://github.com/michalbe/gimmevents) uses [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) and custom pairing system to send events from one browser to another. 13 | 14 | ### Keyboard events 15 | ![gimmevents-keyboard.gif](gimmevents-keyboard.gif) 16 | 17 | ### Mouse events 18 | ![gimmevents-mouse.gif](gimmevents-mouse.gif) 19 | 20 | ## API 21 | ### Emitter 22 | ```javascript 23 | const gimmevents = new Gimmevents.Emitter({ 24 | key: my_unique_session_key, 25 | host: host 26 | }); 27 | 28 | gimmevents.register(event); 29 | ``` 30 | 31 | ### Receiver 32 | ```javascript 33 | const gimmevents = new Gimmevents.Receiver({ 34 | key: my_unique_session_key, 35 | host: host 36 | }); 37 | 38 | gimmevents.on(event, (e) => { 39 | // Do stuff 40 | }); 41 | ``` 42 | 43 | ## EVENTS 44 | - keyup 45 | - keydown 46 | - keypress 47 | - mousemove 48 | - mouseup 49 | - mousedown 50 | 51 | ## Development 52 | 53 | ```bash 54 | git clone git@github.com:michalbe/gimmevents.git 55 | cd gimmevents 56 | npm i 57 | npm run dev 58 | ``` 59 | 60 | then open [localhost:7100/emitter.html](http://localhost:7100/emitter.html) in one browser and [localhost:7100/receiver.html](http://localhost:7100/receiver.html) in another. Demo listened to all the keys pressed in one window and sends them to another. 61 | --------------------------------------------------------------------------------