├── client ├── views │ ├── footer.html │ ├── home.html │ ├── demo.html │ ├── connect.html │ └── docs.html ├── .gitignore ├── assets │ ├── footer_lodyas.png │ ├── GitHub-Mark-32px.png │ ├── GitHub-Mark-64px.png │ ├── congruent_outline.png │ ├── GitHub-Mark-Light-32px.png │ ├── GitHub-Mark-Light-64px.png │ ├── app.css │ ├── normalize.css │ └── skeleton.css ├── demo │ ├── assets │ │ ├── peach-gradient-1.jpg │ │ ├── tex │ │ │ └── shadow-circle.png │ │ ├── 321103__nsstudios__blip1.wav │ │ ├── tree1.dae │ │ └── tree2.dae │ └── index.html ├── services │ ├── index.js │ └── proxy-service.js ├── controllers │ ├── index.js │ ├── home-ctrl.js │ ├── docs-ctrl.js │ ├── nav-ctrl.js │ └── connect-ctrl.js ├── app.js ├── lib │ ├── keyboard-listener.js │ ├── listener.js │ ├── gamepad-listener.js │ ├── proxy-controls-client.js │ └── keyboard.polyfill.js └── index.html ├── .gitignore ├── .jshintrc ├── index.js ├── README.md ├── LICENSE ├── package.json └── server └── proxy-controls-server.js /client/views/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | .DS_Store 4 | .gh-pages 5 | .env 6 | -------------------------------------------------------------------------------- /client/assets/footer_lodyas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/proxy-controls-server/master/client/assets/footer_lodyas.png -------------------------------------------------------------------------------- /client/assets/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/proxy-controls-server/master/client/assets/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /client/assets/GitHub-Mark-64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/proxy-controls-server/master/client/assets/GitHub-Mark-64px.png -------------------------------------------------------------------------------- /client/assets/congruent_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/proxy-controls-server/master/client/assets/congruent_outline.png -------------------------------------------------------------------------------- /client/demo/assets/peach-gradient-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/proxy-controls-server/master/client/demo/assets/peach-gradient-1.jpg -------------------------------------------------------------------------------- /client/assets/GitHub-Mark-Light-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/proxy-controls-server/master/client/assets/GitHub-Mark-Light-32px.png -------------------------------------------------------------------------------- /client/assets/GitHub-Mark-Light-64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/proxy-controls-server/master/client/assets/GitHub-Mark-Light-64px.png -------------------------------------------------------------------------------- /client/demo/assets/tex/shadow-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/proxy-controls-server/master/client/demo/assets/tex/shadow-circle.png -------------------------------------------------------------------------------- /client/services/index.js: -------------------------------------------------------------------------------- 1 | var app = require('angular').module('proxyControlsApp'); 2 | 3 | app.factory('ProxyService', require('./proxy-service')); 4 | -------------------------------------------------------------------------------- /client/demo/assets/321103__nsstudios__blip1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/proxy-controls-server/master/client/demo/assets/321103__nsstudios__blip1.wav -------------------------------------------------------------------------------- /client/controllers/index.js: -------------------------------------------------------------------------------- 1 | var app = require('angular').module('proxyControlsApp'); 2 | 3 | app 4 | .controller('ConnectCtrl', require('./connect-ctrl')) 5 | .controller('DocsCtrl', require('./docs-ctrl')) 6 | .controller('HomeCtrl', require('./home-ctrl')) 7 | .controller('NavCtrl', require('./nav-ctrl')); 8 | -------------------------------------------------------------------------------- /client/services/proxy-service.js: -------------------------------------------------------------------------------- 1 | var ProxyControlsClient = require('../lib/proxy-controls-client'); 2 | 3 | module.exports = function (RESOURCES) { 4 | return { 5 | get: function (pairCode) { 6 | return new ProxyControlsClient({url: RESOURCES.SOCKET_PATH, pairCode: pairCode}); 7 | } 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /client/controllers/home-ctrl.js: -------------------------------------------------------------------------------- 1 | module.exports = function ($scope, $window, RESOURCES) { 2 | $scope.demoUrl = RESOURCES.DEMO_URL; 3 | $scope.isMobile = $window.matchMedia('(max-width: 550px)').matches; 4 | 5 | $window.document.body.classList.add('bg-dark'); 6 | $scope.$on('$destroy', function () { 7 | $window.document.body.classList.remove('bg-dark'); 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /client/controllers/docs-ctrl.js: -------------------------------------------------------------------------------- 1 | module.exports = function ($scope, $location, $anchorScroll) { 2 | 3 | var originalOffset = $anchorScroll.yOffset; 4 | $anchorScroll.yOffset = 100; 5 | $anchorScroll(); 6 | 7 | $scope.goHash = function (anchor) { 8 | $location.hash(anchor); 9 | $anchorScroll(); 10 | }; 11 | 12 | $scope.$on('$destroy', function () { 13 | $anchorScroll.yOffset = originalOffset; 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": {}, 3 | "bitwise": false, 4 | "browser": true, 5 | "eqeqeq": true, 6 | "esnext": true, 7 | "expr": true, 8 | "forin": true, 9 | "immed": true, 10 | "latedef": "nofunc", 11 | "laxbreak": true, 12 | "maxlen": 100, 13 | "newcap": true, 14 | "noarg": true, 15 | "node": true, 16 | "noempty": true, 17 | "noyield": true, 18 | "quotmark": "single", 19 | "smarttabs": false, 20 | "trailing": true, 21 | "undef": true, 22 | "unused": true, 23 | "white": false 24 | } 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | dotenv = require('dotenv'), 3 | ProxyControlsServer = require('./server/proxy-controls-server'); 4 | 5 | dotenv.config({silent: true}); 6 | 7 | if (process.env.SSL_PORT) { 8 | new ProxyControlsServer({ 9 | port: process.env.PORT, 10 | sslPort: process.env.SSL_PORT, 11 | key: fs.readFileSync(process.env.SSL_KEY_PATH), 12 | cert: fs.readFileSync(process.env.SSL_CERT_PATH), 13 | }); 14 | } else { 15 | new ProxyControlsServer({ 16 | port: process.env.PORT || process.env.npm_package_config_port 17 | }); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proxy Controls Server 2 | 3 | ![Status](https://img.shields.io/badge/status-experimental-orange.svg) 4 | [![License](https://img.shields.io/badge/license-MIT-007ec6.svg)](https://github.com/donmccurdy/proxy-controls-server/blob/master/LICENSE) 5 | 6 | Service to proxy keyboard/gamepad controls between devices, peer-to-peer, over WebRTC. 7 | 8 | ## Overview 9 | 10 | ``` 11 | . 12 | ├── client 13 | │   ├── assets 14 | │   ├── controllers 15 | │   ├── lib 16 | │   ├── services 17 | │   └── views 18 | └── server 19 | ``` 20 | 21 | ## Dependencies 22 | 23 | This project relies on the [SocketPeer](https://github.com/cvan/socketpeer) library to manage WebRTC and WebSocket connections. 24 | 25 | ## Notes 26 | 27 | Two optional dependencies for `ws`, `bufferutil` and `utf-8-validate`, are included. [Both improve performance in certain conditions](https://github.com/websockets/ws#opt-in-for-performance). 28 | -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | 3 | angular.module('proxyControlsApp', [ 4 | require('angular-route'), 5 | require('angular-sanitize'), 6 | require('angular-touch') 7 | ]) 8 | .constant('RESOURCES', { 9 | DEMO_URL: '/demo', 10 | SOCKET_PATH: location.protocol + '//' + location.host + '/socketpeer/' 11 | }) 12 | .config(function ($routeProvider) { 13 | $routeProvider 14 | .when('/', { 15 | templateUrl: 'views/home.html', 16 | controller: 'HomeCtrl' 17 | }) 18 | .when('/docs', { 19 | templateUrl: 'views/docs.html', 20 | controller: 'DocsCtrl' 21 | }) 22 | .when('/connect', { 23 | templateUrl: 'views/connect.html', 24 | controller: 'ConnectCtrl' 25 | }) 26 | .otherwise({redirectTo: '/'}); 27 | }).config(['$locationProvider', function ($locationProvider) { 28 | $locationProvider.hashPrefix(''); 29 | }]); 30 | 31 | require('./controllers'); 32 | require('./services'); 33 | -------------------------------------------------------------------------------- /client/controllers/nav-ctrl.js: -------------------------------------------------------------------------------- 1 | module.exports = function ($scope, $document, $window, $element, $location) { 2 | 3 | // Navigation drawer state. 4 | $scope.navOpen = false; 5 | 6 | // Check if given path is active. 7 | $scope.isActive = function (path) { return path === $location.path(); }; 8 | 9 | // Close navigation if page changes. 10 | $scope.$on('$locationChangeStart', function() { 11 | $window.scroll(0, 0); 12 | $scope.navOpen = false; 13 | }); 14 | 15 | var closeNav = function (e) { 16 | if ($scope.navOpen) { 17 | $scope.$apply(function () { $scope.navOpen = false; }); 18 | e.preventDefault(); 19 | } 20 | }; 21 | 22 | var isolateNav = function (e) { e.stopPropagation(); }; 23 | 24 | // Close navigation if user clicks outside nav. 25 | $element.on('click', isolateNav); 26 | $element.on('touchstart', isolateNav); 27 | $document.on('touchstart', closeNav); 28 | $document.on('click', closeNav); 29 | 30 | $scope.$on('$destroy', function () { 31 | $document.off('touchstart', closeNav); 32 | $document.off('click', closeNav); 33 | }); 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Don McCurdy 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 | -------------------------------------------------------------------------------- /client/lib/keyboard-listener.js: -------------------------------------------------------------------------------- 1 | var Listener = require('./listener'), 2 | util = require('util'); 3 | 4 | require('./keyboard.polyfill'); 5 | 6 | function KeyboardListener () { 7 | Listener.call(this); 8 | 9 | this.title = 'Keyboard'; 10 | this.type = 'keyboard'; 11 | this.component = 'keyboard-controls'; 12 | this.github = 'https://github.com/donmccurdy/aframe-keyboard-controls'; 13 | 14 | this.keys = {}; 15 | } 16 | 17 | util.inherits(KeyboardListener, Listener); 18 | 19 | KeyboardListener.prototype.bind = function () { 20 | Listener.prototype.bind.call(this); 21 | 22 | this.__listeners.keydown = this.onKeydown.bind(this); 23 | this.__listeners.keyup = this.onKeyup.bind(this); 24 | 25 | document.addEventListener('keydown', this.__listeners.keydown); 26 | document.addEventListener('keyup', this.__listeners.keyup); 27 | }; 28 | 29 | KeyboardListener.prototype.onKeydown = function (e) { 30 | if (this.keys[e.code]) return; 31 | this.keys[e.code] = true; 32 | this.emit(this.type, {type: this.type, state: this.keys}); 33 | }; 34 | 35 | KeyboardListener.prototype.onKeyup = function (e) { 36 | if (!this.keys[e.code]) return; 37 | delete this.keys[e.code]; 38 | this.emit(this.type, {type: this.type, state: this.keys}); 39 | }; 40 | 41 | module.exports = KeyboardListener; 42 | -------------------------------------------------------------------------------- /client/lib/listener.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events'), 2 | util = require('util'); 3 | 4 | function Listener () { 5 | EventEmitter.call(this); 6 | 7 | /** @type {string:function} Event types and callbacks for document events. */ 8 | this.__listeners = {}; 9 | 10 | /** @type {string} User-facing label for the listener. */ 11 | this.title = ''; 12 | 13 | /** @type {string} Event type for the listener. */ 14 | this.type = ''; 15 | 16 | /** @type {boolean} Enabled/disabled state. */ 17 | this.enabled = true; 18 | } 19 | 20 | util.inherits(Listener, EventEmitter); 21 | 22 | Listener.prototype.emit = function () { 23 | if (!this.enabled) return; 24 | EventEmitter.prototype.emit.apply(this, arguments); 25 | }; 26 | 27 | Listener.prototype.bind = function () {}; 28 | 29 | Listener.prototype.unbind = function () { 30 | for (var event in this.__listeners) { 31 | if (this.__listeners.hasOwnProperty(event)) { 32 | document.removeEventListener(event, this.__listeners[event]); 33 | delete this.__listeners[event]; 34 | } 35 | } 36 | }; 37 | 38 | Listener.prototype.destroy = function () { 39 | this.unbind(); 40 | }; 41 | 42 | Listener.prototype.isEnabled = function () { 43 | return this.enabled; 44 | }; 45 | 46 | Listener.prototype.setEnabled = function (enabled) { 47 | this.enabled = !!enabled; 48 | }; 49 | 50 | module.exports = Listener; 51 | -------------------------------------------------------------------------------- /client/views/home.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |

7 |
Mobile WebVR
8 |
+Interactivity
9 | 10 |

11 | 12 |

13 | Connect input devices from your desktop to your mobile phone with WebRTC. 14 |

15 | 16 | 20 |
21 | 22 |
23 |

24 | Developing with A-Frame? 25 | Use the 26 | 27 | proxy-controls GitHub logo 32 | 33 | component in your scene. Your demo will use this site as the backend (it’s free), so 34 | there’s no need to run your own server. 35 | Read more. 36 |

37 |
38 | 39 |
40 | -------------------------------------------------------------------------------- /client/lib/gamepad-listener.js: -------------------------------------------------------------------------------- 1 | var Listener = require('./listener'), 2 | util = require('util'); 3 | 4 | function GamepadListener () { 5 | Listener.call(this); 6 | 7 | this.title = 'Gamepad'; 8 | this.type = 'gamepad'; 9 | this.component = 'gamepad-controls'; 10 | this.github = 'https://github.com/donmccurdy/aframe-gamepad-controls'; 11 | } 12 | 13 | util.inherits(GamepadListener, Listener); 14 | 15 | GamepadListener.prototype.bind = function () { 16 | // TODO - implement unbind() 17 | if (this.__bound) return; 18 | this.__bound = true; 19 | 20 | Listener.prototype.bind.call(this); 21 | 22 | var publish = function () { 23 | var gamepads = []; 24 | for (var i = 0; i < 4; i++) { 25 | var gamepad = navigator.getGamepads()[i]; 26 | if (gamepad) { 27 | gamepads.push(cloneGamepad(gamepad)); 28 | } 29 | } 30 | if (gamepads.length) { 31 | this.emit(this.type, {type: this.type, state: gamepads}); 32 | } 33 | window.requestAnimationFrame(publish); 34 | }.bind(this); 35 | 36 | var cloneGamepad = function (gamepad) { 37 | var clone = { 38 | axes: gamepad.axes, 39 | buttons: [], 40 | connected: gamepad.connected, 41 | id: gamepad.id, 42 | index: gamepad.index, 43 | mapping: gamepad.mapping, 44 | timestamp: gamepad.timestamp, 45 | }; 46 | 47 | for (var i = 0; i < gamepad.buttons.length; i++) { 48 | clone.buttons.push({ 49 | pressed: gamepad.buttons[i].pressed, 50 | value: gamepad.buttons[i].value 51 | }); 52 | } 53 | 54 | return clone; 55 | }; 56 | 57 | window.requestAnimationFrame(publish); 58 | }; 59 | 60 | GamepadListener.prototype.unbind = function () { 61 | // TODO 62 | }; 63 | 64 | module.exports = GamepadListener; 65 | -------------------------------------------------------------------------------- /client/controllers/connect-ctrl.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | 3 | var INTERVAL = 1000, 4 | DEFAULT_SCOPE = { 5 | pairCode: '', 6 | server: {connected: false}, 7 | peer: {protocol: '', connected: false, latency: 0}, 8 | listeners: [], 9 | protocolLabels: { 10 | rtc: 'WebRTC', 11 | socket: 'WebSocket' 12 | } 13 | }; 14 | 15 | module.exports = function ($scope, $route, $interval, ProxyService, RESOURCES) { 16 | var proxyService = null; 17 | 18 | $scope.demoUrl = RESOURCES.DEMO_URL; 19 | 20 | angular.merge($scope, angular.copy(DEFAULT_SCOPE)); 21 | 22 | $scope.connect = function (pairCode) { 23 | if (proxyService) { 24 | throw new Error('Already connected to a client.'); 25 | } else if (pairCode) { 26 | $scope.pairCode = pairCode; 27 | proxyService = ProxyService.get(pairCode); 28 | } 29 | }; 30 | 31 | $scope.disconnect = function () { 32 | proxyService.destroy(); 33 | proxyService = null; 34 | this.reset(); 35 | }.bind(this); 36 | 37 | this.reset = function () { 38 | angular.merge($scope, angular.copy(DEFAULT_SCOPE)); 39 | }; 40 | 41 | // Poll for changes, because events emitted by proxy service and underlying 42 | // SocketPeer object aren't enough to indicate when server connection status 43 | // has changed. 44 | var intervalPromise = $interval(function () { 45 | if (!proxyService) return; 46 | $scope.server.connected = proxyService.isServerConnected(); 47 | $scope.peer.protocol = proxyService.getPeerProtocol(); 48 | $scope.peer.connected = proxyService.isPeerConnected(); 49 | $scope.peer.latency = proxyService.getPeerLatency(); 50 | $scope.listeners = proxyService.listeners; 51 | }, INTERVAL); 52 | 53 | $scope.$on('$destroy', function () { 54 | if (proxyService) $scope.disconnect(); 55 | $interval.cancel(intervalPromise); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proxy-controls-server", 3 | "version": "0.0.1", 4 | "description": "Service to proxy keyboard/gamepad controls between devices, peer-to-peer, over WebRTC.", 5 | "main": "server/proxy-controls-server.js", 6 | "browser": "client/lib/proxy-controls-client.js", 7 | "engines": { 8 | "node": "6" 9 | }, 10 | "config": { 11 | "port": 3000 12 | }, 13 | "now": { 14 | "alias": "proxy-controls.donmccurdy.com", 15 | "public": true 16 | }, 17 | "scripts": { 18 | "start": "node index.js", 19 | "build": "browserify client/app.js -o client/bundle.js && gzip -f client/bundle.js ", 20 | "dev": "watchify client/app.js -o client/bundle.js -d & nodemon index.js", 21 | "test": "karma start ./tests/karma.conf.js", 22 | "postversion": "git push && git push --tags", 23 | "deploy": "npm run build && now" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/donmccurdy/proxy-controls-server.git" 28 | }, 29 | "keywords": [ 30 | "vr", 31 | "webvr", 32 | "webrtc", 33 | "gamepad", 34 | "keyboard", 35 | "aframe", 36 | "aframe-vr", 37 | "mozvr" 38 | ], 39 | "author": "Don McCurdy ", 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/donmccurdy/proxy-controls-server/issues" 43 | }, 44 | "homepage": "https://github.com/donmccurdy/proxy-controls-server#readme", 45 | "dependencies": { 46 | "angular": "^1.4.8", 47 | "angular-route": "^1.4.8", 48 | "angular-sanitize": "^1.4.8", 49 | "angular-touch": "^1.4.8", 50 | "browserify": "^12.0.1", 51 | "dotenv": "^2.0.0", 52 | "koa": "^1.1.2", 53 | "koa-cors": "0.0.16", 54 | "koa-force-ssl": "0.0.5", 55 | "koa-route": "^2.4.2", 56 | "koa-static": "^2.0.0", 57 | "moniker": "^0.1.2", 58 | "socketpeer": "donmccurdy/socketpeer#fork-master" 59 | }, 60 | "optionalDependencies": { 61 | "bufferutil": "^1.2.1", 62 | "utf-8-validate": "^1.2.1" 63 | }, 64 | "devDependencies": { 65 | "envify": "^3.4.0", 66 | "nodemon": "^1.8.1", 67 | "watchify": "^3.7.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /client/views/demo.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 8 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 |
-------------------------------------------------------------------------------- /server/proxy-controls-server.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | http = require('http'), 3 | https = require('https'), 4 | Koa = require('koa'), 5 | cors = require('koa-cors'), 6 | route = require('koa-route'), 7 | resources = require('koa-static'), 8 | forceSSL = require('koa-force-ssl'), 9 | SocketPeerServer = require('socketpeer'), 10 | moniker = require('moniker'); 11 | 12 | /** 13 | * Server to proxy keyboard/gamepad controls between devices, peer-to-peer, 14 | * over WebRTC. Uses WebSockets as a fallback connection. 15 | * 16 | * Additionally, the ProxyControlsServer is responsible for serving the client 17 | * UI for the host machine, which receives user input to be sent to the remote 18 | * client / viewing device. 19 | * 20 | * @param {Object} options Server configuration options. 21 | */ 22 | function ProxyControlsServer (options) { 23 | /** @type {Koa} Koa application, to serve client UI and AJAX endpoints. */ 24 | this.app = new Koa(); 25 | 26 | /** @type {http.Server} Server, to support both Koa and SocketPeerServer. */ 27 | this.server = null; 28 | 29 | if (options.sslPort) { 30 | assert(options.key, 'key required for SSL.'); 31 | assert(options.cert, 'cert required for SSL.'); 32 | 33 | this.app.use(forceSSL()); 34 | http.createServer(this.app.callback()).listen(options.port); 35 | this.server = https.createServer({ 36 | key: options.key, 37 | cert: options.cert 38 | }, this.app.callback()).listen(options.sslPort); 39 | } else { 40 | this.server = this.app.listen(options.port); 41 | } 42 | 43 | this.app 44 | .use(cors({origin: true})) 45 | .use(resources('client')) 46 | .use(route.get('/ajax/nearby', this.routeNearby())) 47 | .use(route.get('/ajax/pair-code', this.routePairCode())); 48 | 49 | /** @type {SocketPeerServer} WebSocket / WebRTC connection broker. */ 50 | this.socketServer = new SocketPeerServer({ 51 | httpServer: this.server, 52 | serveLibrary: false 53 | }); 54 | 55 | console.info('ProxyControlsServer listening on port %d.', options.sslPort || options.port); 56 | } 57 | 58 | /** 59 | * Suggests nearby peers, assuming same public IP. 60 | */ 61 | ProxyControlsServer.prototype.routeNearby = function () { 62 | return function *() { 63 | // TODO - Implement. 64 | this.body = {count: 0, peers: []}; 65 | }; 66 | }; 67 | 68 | /** 69 | * Returns a unique pair code for client. 70 | */ 71 | ProxyControlsServer.prototype.routePairCode = function () { 72 | return function *() { 73 | // TODO - Verify that pair code is not already in the waiting pool, and that 74 | // the code hasn't been assigned to another client in the last ~60s. 75 | this.body = {pairCode: moniker.choose()}; 76 | }; 77 | }; 78 | 79 | module.exports = ProxyControlsServer; 80 | -------------------------------------------------------------------------------- /client/views/connect.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Get Started

5 | 6 |

With this tab in focus, keyboard and gamepad events can be sent to a remote client.

7 | 8 |
9 |

Notice

10 |

11 | You're joining as a host device (the one that has a keyboard 12 | or gamepad already connected) and appear to be using a mobile phone. 13 | Did you mean to open a demo VR scene, 14 | or to visit this page from your PC? 15 |

16 |
17 | 18 | 20 |

Pairing

21 |

Look for your pair code on the remote device, then enter it here to connect.

22 |
23 |
24 | 30 |
31 |
32 | 34 |
35 |
36 |
37 |

Waiting...

38 |
39 | Waiting for remote device to connect with code “{{ pairCode }}”. 40 |
41 |
42 |
43 |

Connected.

44 |
45 | Okay, the input devices(s) selected below are now connected to your remote device. Have fun. 46 |
47 | 48 | 49 |
50 | 51 | 53 |

Connection

54 |
55 |
56 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 73 | 74 | 75 | 76 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
Server Status 61 | 62 | {{ server.connected ? 'Connected' : 'Disconnected' }} 63 | 64 |
Remote Device 69 | 70 | {{ peer.connected ? 'Connected' : 'Disconnected' }} 71 | 72 |
Protocol 77 | 80 | {{ peer.protocol ? protocolLabels[peer.protocol] : 'Disconnected' }} 81 | 82 |
Latency{{ peer.latency || '∞' }}ms
90 |
91 |
92 | 93 | 95 |
96 |

Input Devices

97 |
98 | 103 |
{{ listener.title }}
104 | 108 |
109 |
110 | 111 |
112 |
-------------------------------------------------------------------------------- /client/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ProxyControls • Demo 6 | 7 | 8 | 9 | 10 | 43 | 44 | 45 | 46 | 50 | 57 | 58 | 59 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | 88 | 93 | 94 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | ProxyControls.js 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 35 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 54 |
55 |
56 |

57 | 58 | Beta 59 |

60 | 61 | 72 |
73 |
74 | 75 | 77 |
78 | 79 | 81 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /client/views/docs.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Docs

5 | 6 | 13 | 14 |
15 |

Overview

16 | 17 |

18 | ProxyControls shares events from input devices (keyboard, gamepad, etc.) 19 | with a remote device. While viewing a WebVR site on your phone with Google 20 | Cardboard, you can use a USB gamepad — connected to your PC — to 21 | move around. 22 |

23 | 24 |

25 | This project works great with Mozilla’s new WebVR library, A-Frame. 26 |

27 | 28 |
29 | 30 |
31 | 32 |
33 |

Host Device

34 | 35 |

36 | For your host (the machine connected directly to the input device), this 37 | website is all you need: 38 |

39 | 40 |

Connect as host

41 | 42 |
43 | 44 |
45 | 46 |
47 |

Remote Device + A-Frame VR

48 | 49 |

50 | For your remote device (e.g. phone) use a proxy-controls 51 | component to receive input events: 52 |

53 | 54 | 67 | 68 |

69 | You’ll also need an input device component to apply 70 | those events and manipulate the scene. Here are a few client packages, 71 | designed for A-Frame: 72 |

73 | 74 | 101 | 102 |

Example:

103 | 104 |
<a-scene proxy-controls>
105 |   <a-cube></a-cube>
106 |   <a-entity camera
107 |             gamepad-controls
108 |             keyboard-controls>
109 |   </a-entity>
110 | </a-scene>
111 | 
112 | 113 |
114 | 115 |
116 | 117 |
118 |

Remote Device, without A-Frame

119 | 120 |

121 | There’s no official pre-packaged support for ProxyControls outside of the 122 | A-Frame environment yet. Feel free to build your own, or 123 | open an issue on GitHub 124 | if you’re interested in discussing an idea. 125 |

126 |
127 | 128 |
129 | 130 |
131 |

WebRTC + Browser Compatibility

132 | 133 |

134 | ProxyControls uses WebRTC's peer-to-peer DataChannel API to reduce 135 | connection latency. The WebRTC spec is not finalized, and 136 | not yet 137 | supported in all browsers. When WebRTC is not available, ProxyControls 138 | will automatically switch to WebSockets. 139 |

140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 |
BrowserStatus (Jan. 2016)
ChromeWebRTC + WebSockets
FirefoxWebRTC + WebSockets
OperaWebRTC + WebSockets
Safari / iOSWebSockets
EdgeWebSockets
IE 11WebSockets
154 | 155 |
156 | 157 |
158 |
159 | -------------------------------------------------------------------------------- /client/lib/proxy-controls-client.js: -------------------------------------------------------------------------------- 1 | var SocketPeer = require('socketpeer'), 2 | EventEmitter = require('events'), 3 | util = require('util'), 4 | KeyboardListener = require('./keyboard-listener'), 5 | GamepadListener = require('./gamepad-listener'); 6 | 7 | /** 8 | * Polling configuration. 9 | * 10 | * The purpose of polling on WebSockets is keep the socket from closing on 11 | * certain hosts, e.g. Heroku. A long interval (1s) is fine for this. When 12 | * polling on WebRTC, the goal is to keep the DataChannel primed – latency 13 | * seems to degrade when data is sent less frequently. WebRTC requires higher- 14 | * frequency polling. 15 | * 16 | * In both cases, polling also allows us to measure average latency. 17 | * 18 | * @type {string: {BUFFER_SIZE: number, INTERVAL: number}} 19 | */ 20 | var POLLING = { 21 | rtc: {BUFFER_SIZE: 50, INTERVAL: 20}, 22 | socket: {BUFFER_SIZE: 5, INTERVAL: 1000} 23 | }; 24 | 25 | /** 26 | * UI controller for client running on host machine. Records user input and 27 | * forwards events to remote client / viewer application. 28 | * 29 | * Options: 30 | * - url: (required) URL of ProxyControlsServer instance. 31 | * - pairCode: (required) Identifier for this client. 32 | * 33 | * @param {Object} options Client configuration. 34 | */ 35 | var ProxyControlsClient = function (options) { 36 | EventEmitter.call(this); 37 | 38 | /** @type {SocketPeer} WebRTC connection. */ 39 | this.peer = new SocketPeer({ 40 | url: options.url, 41 | pairCode: options.pairCode, 42 | socketFallback: true 43 | }); 44 | 45 | /** @type {string:Listener} Listeners bound to document events. */ 46 | this.listeners = [ 47 | new KeyboardListener(), 48 | new GamepadListener() 49 | ]; 50 | 51 | /** @type {string:boolean} Keyboard state, [key]->true. */ 52 | this.keys = {}; 53 | 54 | /** @type {Array} Circular array of recent ping measurements. */ 55 | this.pingList = []; 56 | 57 | /** @type {number} Index at which next polling results are inserted. */ 58 | this.pingListIndex = 0; 59 | 60 | /** @type {number} Interval ID for ping / latency polling. */ 61 | this.pingIntervalID = 0; 62 | 63 | this.onConnect = this.onConnect.bind(this); 64 | this.onDisconnect = this.onDisconnect.bind(this); 65 | this.onClose = this.onClose.bind(this); 66 | this.onUpgrade = this.onUpgrade.bind(this); 67 | this.onDowngrade = this.onDowngrade.bind(this); 68 | this.onConnectError = this.onConnectError.bind(this); 69 | this.onConnectTimeout = this.onConnectTimeout.bind(this); 70 | this.onData = this.onData.bind(this); 71 | this.onError = this.onError.bind(this); 72 | 73 | this.bindEvents(); 74 | }; 75 | 76 | util.inherits(ProxyControlsClient, EventEmitter); 77 | 78 | /** 79 | * Initializes SocketPeer connection with broker server, and begins listening 80 | * for peer connections. 81 | */ 82 | ProxyControlsClient.prototype.bindEvents = function () { 83 | this.peer.on('connect', this.onConnect); 84 | this.peer.on('connect_error', this.onConnectError); 85 | this.peer.on('connect_timeout', this.onConnectTimeout); 86 | this.peer.on('upgrade', this.onUpgrade); 87 | this.peer.on('error', this.onError); 88 | this.peer.on('data', this.onData); 89 | this.peer.on('disconnect', this.onDisconnect); 90 | this.peer.on('close', this.onClose); 91 | 92 | var self = this; 93 | this.listeners.forEach(function (listener) { 94 | listener.on(listener.type, function (e) { 95 | self.peer.send(e); 96 | self.emit(listener.type, e); 97 | console.log('publish(%s)', JSON.stringify(e, null, 2)); 98 | }); 99 | }); 100 | }; 101 | 102 | /* Initialization 103 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 104 | 105 | /** 106 | * Begins sending pings to poll for latency, and also to keep the DataChannel 107 | * or WebSocket active. Resets polling, if it has already begun. 108 | * 109 | * Purpose: Some hosts (e.g. Heroku) will close WebSockets if they are 110 | * inactive for too long. On the other hand, WebRTC DataChannels appear to 111 | * perform better (lower average latency) if they're kept active. So in both 112 | * cases, we want to keep data moving through. 113 | */ 114 | ProxyControlsClient.prototype.resetPings = function () { 115 | // Cancel existing polling. 116 | if (this.pingIntervalID) { 117 | clearInterval(this.pingIntervalID); 118 | } 119 | 120 | // Only poll while connected. 121 | var protocol = this.getPeerProtocol(); 122 | if (!protocol) return; 123 | 124 | this.pingListIndex = 0; 125 | this.pingList = []; 126 | 127 | // Ping remote peer at regular intervals. 128 | this.pingIntervalID = setInterval(function () { 129 | if (this.getPeerProtocol()) { 130 | this.peer.send({type: 'ping', timestamp: Date.now()}); 131 | } else { 132 | this.resetPings(); 133 | } 134 | }.bind(this), POLLING[protocol].INTERVAL); 135 | 136 | console.info('Now polling every %dms', POLLING[protocol].INTERVAL); 137 | }; 138 | 139 | /** 140 | * Removes all event bindings and destroys dependencies. 141 | */ 142 | ProxyControlsClient.prototype.destroy = function () { 143 | if (this.pingIntervalID) { 144 | clearInterval(this.pingIntervalID); 145 | } 146 | 147 | this.listeners.forEach(function (listener) { 148 | listener.destroy(); 149 | }); 150 | 151 | this.peer.close(); 152 | }; 153 | 154 | /* Event bindings 155 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 156 | 157 | ProxyControlsClient.prototype.onConnect = function () { 158 | console.info('connect()'); 159 | this.listeners.forEach(function (listener) { listener.bind(); }); 160 | this.resetPings(); 161 | 162 | }; 163 | 164 | ProxyControlsClient.prototype.onConnectError = function () { 165 | console.error('connect_error()'); 166 | }; 167 | 168 | ProxyControlsClient.prototype.onConnectTimeout = function () { 169 | console.warn('connect_timeout()'); 170 | }; 171 | 172 | ProxyControlsClient.prototype.onDisconnect = function () { 173 | console.info('disconnect()'); 174 | this.listeners.forEach(function (listener) { listener.unbind(); }); 175 | this.resetPings(); 176 | }; 177 | 178 | ProxyControlsClient.prototype.onData = function (event) { 179 | if (event.type === 'ping') { 180 | var protocol = this.getPeerProtocol(); 181 | if (!protocol) return; 182 | this.pingList[this.pingListIndex] = (Date.now() - event.timestamp) / 2; 183 | this.pingListIndex = (this.pingListIndex + 1) % POLLING[protocol].BUFFER_SIZE; 184 | } else { 185 | console.log('data(%s)', JSON.stringify(event, null, 2)); 186 | } 187 | }; 188 | 189 | ProxyControlsClient.prototype.onError = function () { 190 | console.error('error()'); 191 | }; 192 | 193 | ProxyControlsClient.prototype.onClose = function () { 194 | console.info('close()'); 195 | this.listeners.forEach(function (listener) { listener.unbind(); }); 196 | this.resetPings(); 197 | }; 198 | 199 | ProxyControlsClient.prototype.onUpgrade = function () { 200 | console.info('upgrade()'); 201 | this.resetPings(); 202 | }; 203 | 204 | ProxyControlsClient.prototype.onDowngrade = function () { 205 | console.info('downgrade()'); 206 | this.resetPings(); 207 | }; 208 | 209 | 210 | /* Accessors 211 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 212 | 213 | ProxyControlsClient.prototype.isServerConnected = function () { 214 | return this.peer.socket.readyState === WebSocket.OPEN; 215 | }; 216 | 217 | ProxyControlsClient.prototype.isPeerConnected = function () { 218 | return this.peer.peerConnected; 219 | }; 220 | 221 | ProxyControlsClient.prototype.getPeerProtocol = function () { 222 | if (this.peer.rtcConnected) { 223 | return 'rtc'; 224 | } else if (this.peer.socketConnected && this.peer.peerConnected) { 225 | return 'socket'; 226 | } 227 | return null; 228 | }; 229 | 230 | ProxyControlsClient.prototype.getPeerLatency = function () { 231 | if (!this.pingList.length) return NaN; 232 | var avgLatency = 0; 233 | for (var i = 0; i < this.pingList.length; i++) { 234 | avgLatency += this.pingList[i]; 235 | } 236 | return Math.round(avgLatency / this.pingList.length); 237 | }; 238 | 239 | module.exports = ProxyControlsClient; 240 | -------------------------------------------------------------------------------- /client/assets/app.css: -------------------------------------------------------------------------------- 1 | /* Client CSS 2 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 3 | 4 | /* Page layout 5 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 6 | 7 | html { 8 | background: #222; 9 | } 10 | 11 | body { 12 | background: #FFF; 13 | min-height: 100vh; 14 | overflow-y: scroll; 15 | box-sizing: border-box; 16 | padding-top: 60px; 17 | } 18 | 19 | .container { 20 | max-width: 850px; 21 | } 22 | 23 | .view { 24 | padding-top: 2rem; 25 | } 26 | 27 | section { 28 | margin-bottom: 6rem; 29 | } 30 | 31 | @media screen and (min-width: 750px) { 32 | .view { padding-top: 4rem; } 33 | } 34 | 35 | /* Typography 36 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 37 | 38 | h1 { font-size: 5.0rem; } 39 | h2 { font-size: 3.6rem; } 40 | h3 { font-size: 3.0rem; } 41 | h4 { font-size: 2.4rem; } 42 | body { font-size: 1.8rem; } 43 | 44 | p { font-weight: 300; line-height: 1.7em; } 45 | 46 | a { text-decoration: none; } 47 | a:hover { text-decoration: underline; } 48 | 49 | .caps { font-size: 1.5rem; font-weight: 400; } 50 | .a-disabled { color: #888; } 51 | .a-icon { height: 2rem; vertical-align: text-top; } 52 | .a-with-icon { white-space: nowrap; } 53 | 54 | /* Flexbox 55 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 56 | 57 | .flex { 58 | display: -webkit-flex; 59 | display: -moz-flex; 60 | display: -ms-flex; 61 | display: flex; 62 | } 63 | 64 | .flex-column { 65 | -webkit-flex-direction: column; 66 | -moz-flex-direction: column; 67 | -ms-flex-direction: column; 68 | flex-direction: column; 69 | } 70 | 71 | .flex-grow { 72 | -webkit-flex-grow: 1; 73 | -moz-flex-grow: 1; 74 | -ms-flex-grow: 1; 75 | flex-grow: 1; 76 | } 77 | 78 | .flex-center { 79 | -webkit-justify-content: center; 80 | -moz-justify-content: center; 81 | -ms-justify-content: center; 82 | justify-content: center; 83 | } 84 | 85 | .flex-space-around { 86 | -webkit-justify-content: space-around; 87 | -moz-justify-content: space-around; 88 | -ms-justify-content: space-around; 89 | justify-content: space-around; 90 | } 91 | 92 | /* Header 93 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 94 | 95 | header { 96 | position: fixed; 97 | 98 | top: 0; 99 | width: 100%; 100 | height: 60px; 101 | line-height: 60px; 102 | background: #222; 103 | z-index: 1000; 104 | } 105 | 106 | header h4, 107 | header nav { 108 | margin: 0; 109 | line-height: inherit; 110 | display: inline-block; 111 | } 112 | 113 | header > .container { 114 | position: static; 115 | } 116 | 117 | .logo, 118 | .logo:hover { 119 | color: #FFF; 120 | text-decoration: none; 121 | } 122 | 123 | .version { 124 | font-size: 0.4em; 125 | font-weight: 600; 126 | text-transform: uppercase; 127 | position: relative; 128 | bottom: 1em; 129 | color: #1EAEDB; 130 | } 131 | 132 | /* Navigation 133 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 134 | 135 | .menu-btn { 136 | position: absolute; 137 | right: 0; 138 | top: 0; 139 | height: 60px; 140 | padding: 0 30px; 141 | } 142 | 143 | .menu-btn:active { background: #444; } 144 | 145 | .menu-btn:before { 146 | content: ""; 147 | position: absolute; 148 | left: 15px; 149 | top: 22px; 150 | width: 30px; 151 | height: 0.12em; 152 | background: #F0F0F0; 153 | box-shadow: 154 | 0 0.35em 0 0 #F0F0F0, 155 | 0 0.7em 0 0 #F0F0F0; 156 | } 157 | 158 | nav { 159 | position: absolute; 160 | top: 60px; 161 | right: -250px; 162 | width: 250px; 163 | height: calc(100vh - 60px); 164 | 165 | -webkit-transition: right 0.2s ease; 166 | -moz-transition: right 0.2s ease; 167 | -ms-transition: right 0.2s ease; 168 | transition: right 0.2s ease; 169 | 170 | -webkit-backdrop-filter: blur(5px); 171 | -moz-backdrop-filter: blur(5px); 172 | -ms-backdrop-filter: blur(5px); 173 | backdrop-filter: blur(5px); 174 | background: rgba(40,40,40,0.95); 175 | } 176 | 177 | nav.open { right: 0; } 178 | 179 | .nav-link { 180 | display: block; 181 | height: 60px; 182 | box-sizing: border-box; 183 | border-left: 5px solid transparent; 184 | padding: 0 1em; 185 | font-size: 1.8rem; 186 | font-weight: 400; 187 | color: #FFF; 188 | text-decoration: none; 189 | } 190 | 191 | .nav-link.active { border-left: 5px solid #33C3F0; } 192 | 193 | .nav-link:hover, 194 | .nav-link:active { 195 | color: #FFF; 196 | text-decoration: none; 197 | 198 | -webkit-backdrop-filter: blur(5px); 199 | -moz-backdrop-filter: blur(5px); 200 | -ms-backdrop-filter: blur(5px); 201 | backdrop-filter: blur(5px); 202 | background: rgba(34, 34, 34, 0.5); 203 | } 204 | 205 | @media screen and (min-width: 750px) { 206 | .menu-btn { display: none; } 207 | 208 | nav { 209 | position: static; 210 | width: auto; 211 | height: 60px; 212 | background: none; 213 | } 214 | 215 | .nav-link { 216 | display: inline-block; 217 | float: left; 218 | border-left: 0; 219 | } 220 | 221 | .nav-link.active { 222 | border-left: 0; 223 | border-bottom: 5px solid #33C3F0; 224 | } 225 | 226 | .nav-link:hover, 227 | .nav-link:active { 228 | -webkit-backdrop-filter: none; 229 | -moz-backdrop-filter: none; 230 | -ms-backdrop-filter: none; 231 | backdrop-filter: none; 232 | background: #444; 233 | } 234 | 235 | } 236 | 237 | /* Footer 238 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 239 | 240 | footer { 241 | padding: 3rem 0; 242 | font-size: 1.5rem; 243 | background: #222222; 244 | color: #bfbfbf; 245 | } 246 | 247 | .footer-links { 248 | margin-bottom: 1em; 249 | text-align: center; 250 | } 251 | 252 | .footer-link { 253 | display: block; 254 | line-height: 2em; 255 | } 256 | 257 | .footer-desc p:last-child { margin-bottom: 0; } 258 | 259 | footer a.attrib { 260 | color: inherit; 261 | } 262 | 263 | @media screen and (min-width: 550px) { 264 | .footer-links { 265 | margin-top: 0.2em; 266 | margin-bottom: 0; 267 | text-align: left; 268 | } 269 | 270 | .footer-link { 271 | line-height: 1.2em; 272 | } 273 | } 274 | 275 | /* Home 276 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 277 | 278 | body.bg-dark { background: url("footer_lodyas.png"); } 279 | 280 | .home-view { 281 | color: #FFF; 282 | padding: 2rem; 283 | text-align: center; 284 | } 285 | 286 | .home-view > .container { 287 | padding: 0; 288 | } 289 | 290 | .home-title { 291 | font-size: 3.6rem; 292 | font-weight: 300; 293 | text-align: center; 294 | text-shadow: 1px 1px 1px #000; 295 | } 296 | 297 | .home-subtitle { 298 | font-size: 2.2rem; 299 | max-width: 600px; 300 | margin: 0 auto 2rem auto; 301 | } 302 | 303 | .home-caption { 304 | margin-top: 1.5rem; 305 | text-align: left; 306 | } 307 | 308 | .home-view .button { 309 | color: #FFF; 310 | } 311 | 312 | @media screen and (min-width: 550px) { 313 | .home-title { font-size: 5rem; } 314 | .home-subtitle { font-size: 3rem; } 315 | .home-caption { text-align: center; } 316 | } 317 | 318 | /* Getting Started 319 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 320 | 321 | /* Device notice */ 322 | 323 | .device-notice { 324 | border-left: 5px solid #DAA520; 325 | padding-left: 1em; 326 | } 327 | 328 | i.alert { 329 | display: inline-block; 330 | position: relative; 331 | top: 0.1em; 332 | margin-right: 0.2em; 333 | margin-top: -0.5em; 334 | 335 | border: 0.6em solid transparent; 336 | border-bottom: 1em solid #DAA520; 337 | } 338 | 339 | i.alert:after { 340 | position: absolute; 341 | left: -0.1em; 342 | top: 0.1em; 343 | 344 | font-style: normal; 345 | font-size: 0.8em; 346 | content: "!"; 347 | color: #FFF; 348 | } 349 | 350 | @media screen and (min-width: 550px) { 351 | .device-notice { display: none; } 352 | } 353 | 354 | /* Pair code input */ 355 | 356 | input.ng-invalid, 357 | input.ng-invalid:focus { border-color: #ED143D; } 358 | 359 | /* Connection status */ 360 | 361 | .status { 362 | color: #FFF; 363 | border-radius: 4px; 364 | padding: 3rem; 365 | margin-bottom: 1.5rem; 366 | } 367 | 368 | .status.status-pending { background: #DAA520; } 369 | .status.status-connected { background: #33C3F0; } 370 | 371 | 372 | .status .button { 373 | color: #fff; 374 | background: rgba(255,255,255,0.2); 375 | border: 0; 376 | margin: 0; 377 | } 378 | 379 | .status .button:hover { background: #ED143D; } 380 | 381 | /* Modules 382 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 383 | 384 | .module { 385 | background: #F8F8F8; 386 | border-radius: 4px; 387 | padding: 3rem; 388 | } 389 | 390 | .module + .module { margin-top: 1rem; } 391 | .module:last-child { margin-bottom: 2rem; } 392 | 393 | .module ul, 394 | .module li:last-child { 395 | margin-bottom: 0; 396 | } 397 | 398 | .module .button { 399 | height: 30px; 400 | width: 95px; 401 | line-height: 30px; 402 | padding-left: 20px; 403 | padding-right: 20px; 404 | } 405 | 406 | /* Toggles 407 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 408 | 409 | .toggle { 410 | color: #FFF; 411 | padding: 0.3rem 0.5rem; 412 | border-radius: 0.2rem; 413 | } 414 | 415 | .toggle.green { background: #008000; } 416 | .toggle.yellow { background: #DAA520; } 417 | .toggle.red { background: #ED143D; } 418 | 419 | /* Docs 420 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 421 | 422 | .docs-view hr { 423 | margin: 6rem 0; 424 | } 425 | -------------------------------------------------------------------------------- /client/assets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /client/assets/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 128 | color: #222; } 129 | 130 | 131 | /* Typography 132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 133 | h1, h2, h3, h4, h5, h6 { 134 | margin-top: 0; 135 | margin-bottom: 2rem; 136 | font-weight: 300; } 137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 143 | 144 | /* Larger than phablet */ 145 | @media (min-width: 550px) { 146 | h1 { font-size: 5.0rem; } 147 | h2 { font-size: 4.2rem; } 148 | h3 { font-size: 3.6rem; } 149 | h4 { font-size: 3.0rem; } 150 | h5 { font-size: 2.4rem; } 151 | h6 { font-size: 1.5rem; } 152 | } 153 | 154 | p { 155 | margin-top: 0; } 156 | 157 | 158 | /* Links 159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 160 | a { 161 | color: #1EAEDB; } 162 | a:hover { 163 | color: #0FA0CE; } 164 | 165 | 166 | /* Buttons 167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 168 | .button, 169 | button, 170 | input[type="submit"], 171 | input[type="reset"], 172 | input[type="button"] { 173 | display: inline-block; 174 | height: 38px; 175 | padding: 0 30px; 176 | color: #555; 177 | text-align: center; 178 | font-size: 11px; 179 | font-weight: 600; 180 | line-height: 38px; 181 | letter-spacing: .1rem; 182 | text-transform: uppercase; 183 | text-decoration: none; 184 | white-space: nowrap; 185 | background-color: transparent; 186 | border-radius: 4px; 187 | border: 1px solid #bbb; 188 | cursor: pointer; 189 | box-sizing: border-box; } 190 | .button:hover, 191 | button:hover, 192 | input[type="submit"]:hover, 193 | input[type="reset"]:hover, 194 | input[type="button"]:hover, 195 | .button:focus, 196 | button:focus, 197 | input[type="submit"]:focus, 198 | input[type="reset"]:focus, 199 | input[type="button"]:focus { 200 | color: #333; 201 | border-color: #888; 202 | outline: 0; } 203 | .button.button-primary, 204 | button.button-primary, 205 | input[type="submit"].button-primary, 206 | input[type="reset"].button-primary, 207 | input[type="button"].button-primary { 208 | color: #FFF; 209 | background-color: #33C3F0; 210 | border-color: #33C3F0; } 211 | .button.button-primary:hover, 212 | button.button-primary:hover, 213 | input[type="submit"].button-primary:hover, 214 | input[type="reset"].button-primary:hover, 215 | input[type="button"].button-primary:hover, 216 | .button.button-primary:focus, 217 | button.button-primary:focus, 218 | input[type="submit"].button-primary:focus, 219 | input[type="reset"].button-primary:focus, 220 | input[type="button"].button-primary:focus { 221 | color: #FFF; 222 | background-color: #1EAEDB; 223 | border-color: #1EAEDB; } 224 | 225 | 226 | /* Forms 227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 228 | input[type="email"], 229 | input[type="number"], 230 | input[type="search"], 231 | input[type="text"], 232 | input[type="tel"], 233 | input[type="url"], 234 | input[type="password"], 235 | textarea, 236 | select { 237 | height: 38px; 238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 239 | background-color: #fff; 240 | border: 1px solid #D1D1D1; 241 | border-radius: 4px; 242 | box-shadow: none; 243 | box-sizing: border-box; } 244 | /* Removes awkward default styles on some inputs for iOS */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea { 253 | -webkit-appearance: none; 254 | -moz-appearance: none; 255 | appearance: none; } 256 | textarea { 257 | min-height: 65px; 258 | padding-top: 6px; 259 | padding-bottom: 6px; } 260 | input[type="email"]:focus, 261 | input[type="number"]:focus, 262 | input[type="search"]:focus, 263 | input[type="text"]:focus, 264 | input[type="tel"]:focus, 265 | input[type="url"]:focus, 266 | input[type="password"]:focus, 267 | textarea:focus, 268 | select:focus { 269 | border: 1px solid #33C3F0; 270 | outline: 0; } 271 | label, 272 | legend { 273 | display: block; 274 | margin-bottom: .5rem; 275 | font-weight: 600; } 276 | fieldset { 277 | padding: 0; 278 | border-width: 0; } 279 | input[type="checkbox"], 280 | input[type="radio"] { 281 | display: inline; } 282 | label > .label-body { 283 | display: inline-block; 284 | margin-left: .5rem; 285 | font-weight: normal; } 286 | 287 | 288 | /* Lists 289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 290 | ul { 291 | list-style: circle inside; } 292 | ol { 293 | list-style: decimal inside; } 294 | ol, ul { 295 | padding-left: 0; 296 | margin-top: 0; } 297 | ul ul, 298 | ul ol, 299 | ol ol, 300 | ol ul { 301 | margin: 1.5rem 0 1.5rem 3rem; 302 | font-size: 90%; } 303 | li { 304 | margin-bottom: 1rem; } 305 | 306 | 307 | /* Code 308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 309 | code { 310 | padding: .2rem .5rem; 311 | margin: 0 .2rem; 312 | font-size: 90%; 313 | white-space: nowrap; 314 | background: #F1F1F1; 315 | border: 1px solid #E1E1E1; 316 | border-radius: 4px; } 317 | pre > code { 318 | display: block; 319 | padding: 1rem 1.5rem; 320 | white-space: pre; } 321 | 322 | 323 | /* Tables 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | th, 326 | td { 327 | padding: 12px 15px; 328 | text-align: left; 329 | border-bottom: 1px solid #E1E1E1; } 330 | th:first-child, 331 | td:first-child { 332 | padding-left: 0; } 333 | th:last-child, 334 | td:last-child { 335 | padding-right: 0; } 336 | 337 | 338 | /* Spacing 339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 340 | button, 341 | .button { 342 | margin-bottom: 1rem; } 343 | input, 344 | textarea, 345 | select, 346 | fieldset { 347 | margin-bottom: 1.5rem; } 348 | pre, 349 | blockquote, 350 | dl, 351 | figure, 352 | table, 353 | p, 354 | ul, 355 | ol, 356 | form { 357 | margin-bottom: 2.5rem; } 358 | 359 | 360 | /* Utilities 361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 362 | .u-full-width { 363 | width: 100%; 364 | box-sizing: border-box; } 365 | .u-max-full-width { 366 | max-width: 100%; 367 | box-sizing: border-box; } 368 | .u-pull-right { 369 | float: right; } 370 | .u-pull-left { 371 | float: left; } 372 | 373 | 374 | /* Misc 375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 376 | hr { 377 | margin-top: 3rem; 378 | margin-bottom: 3.5rem; 379 | border-width: 0; 380 | border-top: 1px solid #E1E1E1; } 381 | 382 | 383 | /* Clearing 384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 385 | 386 | /* Self Clearing Goodness */ 387 | .container:after, 388 | .row:after, 389 | .u-cf { 390 | content: ""; 391 | display: table; 392 | clear: both; } 393 | 394 | 395 | /* Media Queries 396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 397 | /* 398 | Note: The best way to structure the use of media queries is to create the queries 399 | near the relevant code. For example, if you wanted to change the styles for buttons 400 | on small devices, paste the mobile query code up in the buttons section and style it 401 | there. 402 | */ 403 | 404 | 405 | /* Larger than mobile */ 406 | @media (min-width: 400px) {} 407 | 408 | /* Larger than phablet (also point when grid becomes active) */ 409 | @media (min-width: 550px) {} 410 | 411 | /* Larger than tablet */ 412 | @media (min-width: 750px) {} 413 | 414 | /* Larger than desktop */ 415 | @media (min-width: 1000px) {} 416 | 417 | /* Larger than Desktop HD */ 418 | @media (min-width: 1200px) {} 419 | -------------------------------------------------------------------------------- /client/demo/assets/tree1.dae: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CINEMA4D 15.064 COLLADA Exporter 6 | 7 | 2015-08-28T05:40:04Z 8 | 2015-08-28T05:40:04Z 9 | 10 | Y_UP 11 | 12 | 13 | 14 | tex/shadow-circle.png 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 0.8 0.8 0.8 1 24 | 25 | 26 | 0.2 0.2 0.2 1 27 | 28 | 29 | 0.5 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 0.0627451 0.396078 0.513725 1 41 | 42 | 43 | 0.2 0.2 0.2 1 44 | 45 | 46 | 0.5 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 0.0235294 0.564706 0.607843 1 58 | 59 | 60 | 0.2 0.2 0.2 1 61 | 62 | 63 | 0.5 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 0.164706 0.72549 0.701961 1 75 | 76 | 77 | 0.2 0.2 0.2 1 78 | 79 | 80 | 0.5 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 0.654902 0.47451 0.415686 1 92 | 93 | 94 | 0.2 0.2 0.2 1 95 | 96 | 97 | 0.5 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | ID13 108 | 109 | 110 | 111 | 112 | ID14 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 1 1 1 1 122 | 123 | 124 | 0.88 125 | 126 | 127 | 1 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -40 -80.5 40 40 -80.5 40 40 -80.5 -40 -40 -80.5 -40 -40 -77.5711 47.0711 40 -77.5711 47.0711 45 -77.5711 45 47.0711 -77.5711 40 47.0711 -77.5711 -40 45 -77.5711 -45 40 -77.5711 -47.0711 -40 -77.5711 -47.0711 -45 -77.5711 -45 -47.0711 -77.5711 -40 -47.0711 -77.5711 40 -45 -77.5711 45 -40 -70.5 50 40 -70.5 50 47.0711 -70.5 47.0711 50 -70.5 40 50 -70.5 -40 47.0711 -70.5 -47.0711 40 -70.5 -50 -40 -70.5 -50 -47.0711 -70.5 -47.0711 -50 -70.5 -40 -50 -70.5 40 -47.0711 -70.5 47.0711 -40 70.5 50 40 70.5 50 47.0711 70.5 47.0711 50 70.5 40 50 70.5 -40 47.0711 70.5 -47.0711 40 70.5 -50 -40 70.5 -50 -47.0711 70.5 -47.0711 -50 70.5 -40 -50 70.5 40 -47.0711 70.5 47.0711 -40 77.5711 47.0711 40 77.5711 47.0711 45 77.5711 45 47.0711 77.5711 40 47.0711 77.5711 -40 45 77.5711 -45 40 77.5711 -47.0711 -40 77.5711 -47.0711 -45 77.5711 -45 -47.0711 77.5711 -40 -47.0711 77.5711 40 -45 77.5711 45 -40 80.5 40 40 80.5 40 40 80.5 -40 -40 80.5 -40 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -0.0785214 -0.921027 0.381502 0.0785214 -0.921027 0.381502 0 -0.382683 0.92388 0 0 1 0 0.382683 0.92388 -0.0785214 0.921027 0.381502 0.0785214 0.921027 0.381502 0.156558 -0.912487 0.377964 0.357407 -0.357407 0.862856 0.382683 0 0.92388 0.357407 0.357407 0.862856 0.156558 0.912487 0.377964 0.381502 -0.921027 0.0785214 0.377964 -0.912487 0.156558 0.862856 -0.357407 0.357407 0.92388 0 0.382683 0.862856 0.357407 0.357407 0.377964 0.912487 0.156558 0.381502 0.921027 0.0785214 0.381502 -0.921027 -0.0785214 0.92388 -0.382683 -0 1 0 -0 0.92388 0.382683 -0 0.381502 0.921027 -0.0785214 0.377964 -0.912487 -0.156558 0.862856 -0.357407 -0.357407 0.92388 0 -0.382683 0.862856 0.357407 -0.357407 0.377964 0.912487 -0.156558 0.0785214 -0.921027 -0.381502 0.156558 -0.912487 -0.377964 0.357407 -0.357407 -0.862856 0.382683 0 -0.92388 0.357407 0.357407 -0.862856 0.156558 0.912487 -0.377964 0.0785214 0.921027 -0.381502 -0.0785214 -0.921027 -0.381502 0 -0.382683 -0.92388 0 0 -1 0 0.382683 -0.92388 -0.0785214 0.921027 -0.381502 -0.156558 -0.912487 -0.377964 -0.357407 -0.357407 -0.862856 -0.382683 0 -0.92388 -0.357407 0.357407 -0.862856 -0.156558 0.912487 -0.377964 -0.381502 -0.921027 -0.0785214 -0.377964 -0.912487 -0.156558 -0.862856 -0.357407 -0.357407 -0.92388 0 -0.382683 -0.862856 0.357407 -0.357407 -0.377964 0.912487 -0.156558 -0.381502 0.921027 -0.0785214 -0.381502 -0.921027 0.0785214 -0.92388 -0.382683 -0 -1 0 -0 -0.92388 0.382683 -0 -0.381502 0.921027 0.0785214 -0.377964 -0.912487 0.156558 -0.862856 -0.357407 0.357407 -0.92388 0 0.382683 -0.862856 0.357407 0.357407 -0.377964 0.912487 0.156558 -0.156558 -0.912487 0.377964 -0.357407 -0.357407 0.862856 -0.382683 0 0.92388 -0.357407 0.357407 0.862856 -0.156558 0.912487 0.377964 0 1 -0 0 -1 -0 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 0 0 0 0.0455526 0.835876 0.0455526 0.835876 0 0 0.0911051 0.835876 0.0911051 0 0.908895 0.835876 0.908895 0 0.954447 0.835876 0.954447 0 1 0.835876 1 0.876907 0 0.917938 0.0455526 0.917938 0.0911051 0.917938 0.908895 0.917938 0.954447 0.876907 1 0.958969 0 1 0.0455526 1 0.0911051 1 0.908895 1 0.954447 0.958969 1 1 1 1 0 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 194 |

1 1 3 5 1 2 4 0 1 0 0 0 5 2 2 17 2 5 16 2 4 4 2 1 17 3 5 29 3 7 28 3 6 16 3 4 29 4 7 41 4 9 40 4 8 28 4 6 41 6 9 53 6 11 52 5 10 40 5 8 6 7 13 5 1 2 1 1 12 6 8 13 18 8 14 17 8 5 5 8 2 18 9 14 30 9 15 29 9 7 17 9 5 30 10 15 42 10 16 41 10 9 29 10 7 42 11 16 53 6 17 41 6 9 7 12 19 6 13 13 1 12 18 7 14 19 19 14 20 18 14 14 6 14 13 19 15 20 31 15 21 30 15 15 18 15 14 31 16 21 43 16 22 42 16 16 30 16 15 43 18 22 53 18 23 42 17 16 2 19 3 8 19 2 7 12 1 1 12 0 8 20 2 20 20 5 19 20 4 7 20 1 20 21 5 32 21 7 31 21 6 19 21 4 32 22 7 44 22 9 43 22 8 31 22 6 44 23 9 54 23 11 53 18 10 43 18 8 9 24 13 8 19 2 2 19 12 9 25 13 21 25 14 20 25 5 8 25 2 21 26 14 33 26 15 32 26 7 20 26 5 33 27 15 45 27 16 44 27 9 32 27 7 45 28 16 54 23 17 44 23 9 10 29 19 9 30 13 2 29 18 10 31 19 22 31 20 21 31 14 9 31 13 22 32 20 34 32 21 33 32 15 21 32 14 34 33 21 46 33 22 45 33 16 33 33 15 46 35 22 54 35 23 45 34 16 3 36 3 11 36 2 10 29 1 2 29 0 11 37 2 23 37 5 22 37 4 10 37 1 23 38 5 35 38 7 34 38 6 22 38 4 35 39 7 47 39 9 46 39 8 34 39 6 47 40 9 55 40 11 54 35 10 46 35 8 12 41 13 11 36 2 3 36 12 12 42 13 24 42 14 23 42 5 11 42 2 24 43 14 36 43 15 35 43 7 23 43 5 36 44 15 48 44 16 47 44 9 35 44 7 48 45 16 55 40 17 47 40 9 13 46 19 12 47 13 3 46 18 13 48 19 25 48 20 24 48 14 12 48 13 25 49 20 37 49 21 36 49 15 24 49 14 37 50 21 49 50 22 48 50 16 36 50 15 49 52 22 55 52 23 48 51 16 0 53 3 14 53 2 13 46 1 3 46 0 14 54 2 26 54 5 25 54 4 13 54 1 26 55 5 38 55 7 37 55 6 25 55 4 38 56 7 50 56 9 49 56 8 37 56 6 50 57 9 52 57 11 55 52 10 49 52 8 15 58 13 14 53 2 0 53 12 15 59 13 27 59 14 26 59 5 14 59 2 27 60 14 39 60 15 38 60 7 26 60 5 39 61 15 51 61 16 50 61 9 38 61 7 51 62 16 52 57 17 50 57 9 4 0 19 15 63 13 0 0 18 4 64 19 16 64 20 27 64 14 15 64 13 16 65 20 28 65 21 39 65 15 27 65 14 28 66 21 40 66 22 51 66 16 39 66 15 40 5 22 52 5 23 51 67 16 53 68 25 54 68 24 55 68 10 52 68 0 3 69 0 2 69 25 1 69 24 0 69 10

195 |
196 |
197 |
198 | 199 | 200 | 201 | 0 -50 0 0 50 0 10 -50 -0 10 -50 -0 10 50 -0 10 50 -0 8.66025 -50 -5 8.66025 -50 -5 8.66025 50 -5 8.66025 50 -5 5 -50 -8.66025 5 -50 -8.66025 5 50 -8.66025 5 50 -8.66025 6.12323e-16 -50 -10 6.12323e-16 -50 -10 6.12323e-16 50 -10 6.12323e-16 50 -10 -5 -50 -8.66025 -5 -50 -8.66025 -5 50 -8.66025 -5 50 -8.66025 -8.66025 -50 -5 -8.66025 -50 -5 -8.66025 50 -5 -8.66025 50 -5 -10 -50 -1.22465e-15 -10 -50 -1.22465e-15 -10 50 -1.22465e-15 -10 50 -1.22465e-15 -8.66025 -50 5 -8.66025 -50 5 -8.66025 50 5 -8.66025 50 5 -5 -50 8.66025 -5 -50 8.66025 -5 50 8.66025 -5 50 8.66025 -1.83697e-15 -50 10 -1.83697e-15 -50 10 -1.83697e-15 50 10 -1.83697e-15 50 10 5 -50 8.66025 5 -50 8.66025 5 50 8.66025 5 50 8.66025 8.66025 -50 5 8.66025 -50 5 8.66025 50 5 8.66025 50 5 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 0 -1 -0 1 0 -0 0.866026 0 -0.5 0 1 -0 0.5 0 -0.866026 0 0 -1 -0.5 0 -0.866026 -0.866026 0 -0.5 -1 0 -0 -0.866026 0 0.5 -0.5 0 0.866026 0 0 1 0.5 0 0.866026 0.866026 0 0.5 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 0.5 0.5 0 0.5 0.0669873 0.75 0 0 0 1 0.0833333 1 0.0833333 0 1 0.5 0.933013 0.75 0.25 0.933013 0.166667 1 0.166667 0 0.75 0.933013 0.5 1 0.25 1 0.25 0 0.333333 1 0.333333 0 0.416667 1 0.416667 0 0.5 0 0.933013 0.25 0.583333 1 0.583333 0 0.0669873 0.25 0.75 0.0669873 0.666667 1 0.666667 0 0.25 0.0669873 0.75 1 0.75 0 0.833333 1 0.833333 0 0.916667 1 0.916667 0 1 1 1 0 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 3 4 3 3 4 3 3 4 3 3 4 3 3 4 3 3 4 3 3 4 3 3 4 3 3 4 3 3 4 3 3 4 3 3 4 3 237 |

6 0 2 2 0 1 0 0 0 7 2 6 8 2 5 4 1 4 3 1 3 9 3 8 1 3 0 5 3 7 10 0 9 6 0 2 0 0 0 11 4 11 12 4 10 8 2 5 7 2 6 13 3 12 1 3 0 9 3 8 14 0 13 10 0 9 0 0 0 15 5 15 16 5 14 12 4 10 11 4 11 17 3 13 1 3 0 13 3 12 18 0 12 14 0 13 0 0 0 19 6 17 20 6 16 16 5 14 15 5 15 21 3 9 1 3 0 17 3 13 22 0 8 18 0 12 0 0 0 23 7 19 24 7 18 20 6 16 19 6 17 25 3 2 1 3 0 21 3 9 26 0 7 22 0 8 0 0 0 27 8 20 28 8 13 24 7 18 23 7 19 29 3 1 1 3 0 25 3 2 30 0 21 26 0 7 0 0 0 31 9 23 32 9 22 28 8 13 27 8 20 33 3 24 1 3 0 29 3 1 34 0 25 30 0 21 0 0 0 35 10 27 36 10 26 32 9 22 31 9 23 37 3 28 1 3 0 33 3 24 38 0 20 34 0 25 0 0 0 39 11 30 40 11 29 36 10 26 35 10 27 41 3 20 1 3 0 37 3 28 42 0 28 38 0 20 0 0 0 43 12 32 44 12 31 40 11 29 39 11 30 45 3 25 1 3 0 41 3 20 46 0 24 42 0 28 0 0 0 47 13 34 48 13 33 44 12 31 43 12 32 49 3 21 1 3 0 45 3 25 2 0 1 46 0 24 0 0 0 3 1 36 4 1 35 48 13 33 47 13 34 5 3 7 1 3 0 49 3 21

238 |
239 |
240 |
241 | 242 | 243 | 244 | -50 0 50 50 0 50 -50 0 -50 50 0 -50 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 0 1 -0 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 0 0 0 1 1 1 1 0 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 4 280 |

1 0 3 3 0 2 2 0 1 0 0 0

281 |
282 |
283 |
284 |
285 | 286 | 287 | 288 | 0 0 -0 289 | 0 1 0 -0 290 | 1 0 0 0 291 | 0 0 1 -0 292 | 1 1 1 293 | 294 | 0 180 -0 295 | 0 1 0 -0 296 | 1 0 0 0 297 | 0 0 1 -0 298 | 1 1 1 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 0 50 -0 311 | 0 1 0 -0 312 | 1 0 0 0 313 | 0 0 1 -0 314 | 1 1 1 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 0 0 -0 327 | 0 1 0 -0 328 | 1 0 0 0 329 | 0 0 1 -0 330 | 1 1 1 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 |
348 | -------------------------------------------------------------------------------- /client/lib/keyboard.polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Polyfill for the additional KeyboardEvent properties defined in the D3E and 3 | * D4E draft specifications, by @inexorabletash. 4 | * 5 | * See: https://github.com/inexorabletash/polyfill 6 | */ 7 | (function(global) { 8 | var nativeKeyboardEvent = ('KeyboardEvent' in global); 9 | if (!nativeKeyboardEvent) 10 | global.KeyboardEvent = function KeyboardEvent() { throw TypeError('Illegal constructor'); }; 11 | 12 | global.KeyboardEvent.DOM_KEY_LOCATION_STANDARD = 0x00; // Default or unknown location 13 | global.KeyboardEvent.DOM_KEY_LOCATION_LEFT = 0x01; // e.g. Left Alt key 14 | global.KeyboardEvent.DOM_KEY_LOCATION_RIGHT = 0x02; // e.g. Right Alt key 15 | global.KeyboardEvent.DOM_KEY_LOCATION_NUMPAD = 0x03; // e.g. Numpad 0 or + 16 | 17 | var STANDARD = window.KeyboardEvent.DOM_KEY_LOCATION_STANDARD, 18 | LEFT = window.KeyboardEvent.DOM_KEY_LOCATION_LEFT, 19 | RIGHT = window.KeyboardEvent.DOM_KEY_LOCATION_RIGHT, 20 | NUMPAD = window.KeyboardEvent.DOM_KEY_LOCATION_NUMPAD; 21 | 22 | //-------------------------------------------------------------------- 23 | // 24 | // Utilities 25 | // 26 | //-------------------------------------------------------------------- 27 | 28 | function contains(s, ss) { return String(s).indexOf(ss) !== -1; } 29 | 30 | var os = (function() { 31 | if (contains(navigator.platform, 'Win')) { return 'win'; } 32 | if (contains(navigator.platform, 'Mac')) { return 'mac'; } 33 | if (contains(navigator.platform, 'CrOS')) { return 'cros'; } 34 | if (contains(navigator.platform, 'Linux')) { return 'linux'; } 35 | if (contains(navigator.userAgent, 'iPad') || contains(navigator.platform, 'iPod') || contains(navigator.platform, 'iPhone')) { return 'ios'; } 36 | return ''; 37 | } ()); 38 | 39 | var browser = (function() { 40 | if (contains(navigator.userAgent, 'Chrome/')) { return 'chrome'; } 41 | if (contains(navigator.vendor, 'Apple')) { return 'safari'; } 42 | if (contains(navigator.userAgent, 'MSIE')) { return 'ie'; } 43 | if (contains(navigator.userAgent, 'Gecko/')) { return 'moz'; } 44 | if (contains(navigator.userAgent, 'Opera/')) { return 'opera'; } 45 | return ''; 46 | } ()); 47 | 48 | var browser_os = browser + '-' + os; 49 | 50 | function mergeIf(baseTable, select, table) { 51 | if (browser_os === select || browser === select || os === select) { 52 | Object.keys(table).forEach(function(keyCode) { 53 | baseTable[keyCode] = table[keyCode]; 54 | }); 55 | } 56 | } 57 | 58 | function remap(o, key) { 59 | var r = {}; 60 | Object.keys(o).forEach(function(k) { 61 | var item = o[k]; 62 | if (key in item) { 63 | r[item[key]] = item; 64 | } 65 | }); 66 | return r; 67 | } 68 | 69 | function invert(o) { 70 | var r = {}; 71 | Object.keys(o).forEach(function(k) { 72 | r[o[k]] = k; 73 | }); 74 | return r; 75 | } 76 | 77 | //-------------------------------------------------------------------- 78 | // 79 | // Generic Mappings 80 | // 81 | //-------------------------------------------------------------------- 82 | 83 | // "keyInfo" is a dictionary: 84 | // code: string - name from DOM Level 3 KeyboardEvent code Values 85 | // https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3Events-code.html 86 | // location (optional): number - one of the DOM_KEY_LOCATION values 87 | // keyCap (optional): string - keyboard label in en-US locale 88 | // USB code Usage ID from page 0x07 unless otherwise noted (Informative) 89 | 90 | // Map of keyCode to keyInfo 91 | var keyCodeToInfoTable = { 92 | // 0x01 - VK_LBUTTON 93 | // 0x02 - VK_RBUTTON 94 | 0x03: { code: 'Cancel' }, // [USB: 0x9b] char \x0018 ??? (Not in D3E) 95 | // 0x04 - VK_MBUTTON 96 | // 0x05 - VK_XBUTTON1 97 | // 0x06 - VK_XBUTTON2 98 | 0x06: { code: 'Help' }, // [USB: 0x75] ??? 99 | // 0x07 - undefined 100 | 0x08: { code: 'Backspace' }, // [USB: 0x2a] Labelled Delete on Macintosh keyboards. 101 | 0x09: { code: 'Tab' }, // [USB: 0x2b] 102 | // 0x0A-0x0B - reserved 103 | 0X0C: { code: 'Clear' }, // [USB: 0x9c] NumPad Center (Not in D3E) 104 | 0X0D: { code: 'Enter' }, // [USB: 0x28] 105 | // 0x0E-0x0F - undefined 106 | 107 | 0x10: { code: 'Shift' }, 108 | 0x11: { code: 'Control' }, 109 | 0x12: { code: 'Alt' }, 110 | 0x13: { code: 'Pause' }, // [USB: 0x48] 111 | 0x14: { code: 'CapsLock' }, // [USB: 0x39] 112 | 0x15: { code: 'KanaMode' }, // [USB: 0x88] - "HangulMode" for Korean layout 113 | 0x16: { code: 'HangulMode' }, // [USB: 0x90] 0x15 as well in MSDN VK table ??? 114 | 0x17: { code: 'JunjaMode' }, // (Not in D3E) 115 | 0x18: { code: 'FinalMode' }, // (Not in D3E) 116 | 0x19: { code: 'KanjiMode' }, // [USB: 0x91] - "HanjaMode" for Korean layout 117 | // 0x1A - undefined 118 | 0x1B: { code: 'Escape' }, // [USB: 0x29] 119 | 0x1C: { code: 'Convert' }, // [USB: 0x8a] 120 | 0x1D: { code: 'NonConvert' }, // [USB: 0x8b] 121 | 0x1E: { code: 'Accept' }, // (Not in D3E) 122 | 0x1F: { code: 'ModeChange' }, // (Not in D3E) 123 | 124 | 0x20: { code: 'Space' }, // [USB: 0x2c] 125 | 0x21: { code: 'PageUp' }, // [USB: 0x4b] 126 | 0x22: { code: 'PageDown' }, // [USB: 0x4e] 127 | 0x23: { code: 'End' }, // [USB: 0x4d] 128 | 0x24: { code: 'Home' }, // [USB: 0x4a] 129 | 0x25: { code: 'ArrowLeft' }, // [USB: 0x50] 130 | 0x26: { code: 'ArrowUp' }, // [USB: 0x52] 131 | 0x27: { code: 'ArrowRight' }, // [USB: 0x4f] 132 | 0x28: { code: 'ArrowDown' }, // [USB: 0x51] 133 | 0x29: { code: 'Select' }, // (Not in D3E) 134 | 0x2A: { code: 'Print' }, // (Not in D3E) 135 | 0x2B: { code: 'Execute' }, // [USB: 0x74] (Not in D3E) 136 | 0x2C: { code: 'PrintScreen' }, // [USB: 0x46] 137 | 0x2D: { code: 'Insert' }, // [USB: 0x49] 138 | 0x2E: { code: 'Delete' }, // [USB: 0x4c] 139 | 0x2F: { code: 'Help' }, // [USB: 0x75] ??? 140 | 141 | 0x30: { code: 'Digit0', keyCap: '0' }, // [USB: 0x27] 0) 142 | 0x31: { code: 'Digit1', keyCap: '1' }, // [USB: 0x1e] 1! 143 | 0x32: { code: 'Digit2', keyCap: '2' }, // [USB: 0x1f] 2@ 144 | 0x33: { code: 'Digit3', keyCap: '3' }, // [USB: 0x20] 3# 145 | 0x34: { code: 'Digit4', keyCap: '4' }, // [USB: 0x21] 4$ 146 | 0x35: { code: 'Digit5', keyCap: '5' }, // [USB: 0x22] 5% 147 | 0x36: { code: 'Digit6', keyCap: '6' }, // [USB: 0x23] 6^ 148 | 0x37: { code: 'Digit7', keyCap: '7' }, // [USB: 0x24] 7& 149 | 0x38: { code: 'Digit8', keyCap: '8' }, // [USB: 0x25] 8* 150 | 0x39: { code: 'Digit9', keyCap: '9' }, // [USB: 0x26] 9( 151 | // 0x3A-0x40 - undefined 152 | 153 | 0x41: { code: 'KeyA', keyCap: 'a' }, // [USB: 0x04] 154 | 0x42: { code: 'KeyB', keyCap: 'b' }, // [USB: 0x05] 155 | 0x43: { code: 'KeyC', keyCap: 'c' }, // [USB: 0x06] 156 | 0x44: { code: 'KeyD', keyCap: 'd' }, // [USB: 0x07] 157 | 0x45: { code: 'KeyE', keyCap: 'e' }, // [USB: 0x08] 158 | 0x46: { code: 'KeyF', keyCap: 'f' }, // [USB: 0x09] 159 | 0x47: { code: 'KeyG', keyCap: 'g' }, // [USB: 0x0a] 160 | 0x48: { code: 'KeyH', keyCap: 'h' }, // [USB: 0x0b] 161 | 0x49: { code: 'KeyI', keyCap: 'i' }, // [USB: 0x0c] 162 | 0x4A: { code: 'KeyJ', keyCap: 'j' }, // [USB: 0x0d] 163 | 0x4B: { code: 'KeyK', keyCap: 'k' }, // [USB: 0x0e] 164 | 0x4C: { code: 'KeyL', keyCap: 'l' }, // [USB: 0x0f] 165 | 0x4D: { code: 'KeyM', keyCap: 'm' }, // [USB: 0x10] 166 | 0x4E: { code: 'KeyN', keyCap: 'n' }, // [USB: 0x11] 167 | 0x4F: { code: 'KeyO', keyCap: 'o' }, // [USB: 0x12] 168 | 169 | 0x50: { code: 'KeyP', keyCap: 'p' }, // [USB: 0x13] 170 | 0x51: { code: 'KeyQ', keyCap: 'q' }, // [USB: 0x14] 171 | 0x52: { code: 'KeyR', keyCap: 'r' }, // [USB: 0x15] 172 | 0x53: { code: 'KeyS', keyCap: 's' }, // [USB: 0x16] 173 | 0x54: { code: 'KeyT', keyCap: 't' }, // [USB: 0x17] 174 | 0x55: { code: 'KeyU', keyCap: 'u' }, // [USB: 0x18] 175 | 0x56: { code: 'KeyV', keyCap: 'v' }, // [USB: 0x19] 176 | 0x57: { code: 'KeyW', keyCap: 'w' }, // [USB: 0x1a] 177 | 0x58: { code: 'KeyX', keyCap: 'x' }, // [USB: 0x1b] 178 | 0x59: { code: 'KeyY', keyCap: 'y' }, // [USB: 0x1c] 179 | 0x5A: { code: 'KeyZ', keyCap: 'z' }, // [USB: 0x1d] 180 | 0x5B: { code: 'OSLeft', location: LEFT }, // [USB: 0xe3] 181 | 0x5C: { code: 'OSRight', location: RIGHT }, // [USB: 0xe7] 182 | 0x5D: { code: 'ContextMenu' }, // [USB: 0x65] Context Menu 183 | // 0x5E - reserved 184 | 0x5F: { code: 'Standby' }, // [USB: 0x82] Sleep 185 | 186 | 0x60: { code: 'Numpad0', keyCap: '0', location: NUMPAD }, // [USB: 0x62] 187 | 0x61: { code: 'Numpad1', keyCap: '1', location: NUMPAD }, // [USB: 0x59] 188 | 0x62: { code: 'Numpad2', keyCap: '2', location: NUMPAD }, // [USB: 0x5a] 189 | 0x63: { code: 'Numpad3', keyCap: '3', location: NUMPAD }, // [USB: 0x5b] 190 | 0x64: { code: 'Numpad4', keyCap: '4', location: NUMPAD }, // [USB: 0x5c] 191 | 0x65: { code: 'Numpad5', keyCap: '5', location: NUMPAD }, // [USB: 0x5d] 192 | 0x66: { code: 'Numpad6', keyCap: '6', location: NUMPAD }, // [USB: 0x5e] 193 | 0x67: { code: 'Numpad7', keyCap: '7', location: NUMPAD }, // [USB: 0x5f] 194 | 0x68: { code: 'Numpad8', keyCap: '8', location: NUMPAD }, // [USB: 0x60] 195 | 0x69: { code: 'Numpad9', keyCap: '9', location: NUMPAD }, // [USB: 0x61] 196 | 0x6A: { code: 'NumpadMultiply', keyCap: '*', location: NUMPAD }, // [USB: 0x55] 197 | 0x6B: { code: 'NumpadAdd', keyCap: '+', location: NUMPAD }, // [USB: 0x57] 198 | 0x6C: { code: 'NumpadComma', keyCap: ',', location: NUMPAD }, // [USB: 0x85] 199 | 0x6D: { code: 'NumpadSubtract', keyCap: '-', location: NUMPAD }, // [USB: 0x56] 200 | 0x6E: { code: 'NumpadDecimal', keyCap: '.', location: NUMPAD }, // [USB: 0x63] 201 | 0x6F: { code: 'NumpadDivide', keyCap: '/', location: NUMPAD }, // [USB: 0x54] 202 | 203 | 0x70: { code: 'F1' }, // [USB: 0x3a] 204 | 0x71: { code: 'F2' }, // [USB: 0x3b] 205 | 0x72: { code: 'F3' }, // [USB: 0x3c] 206 | 0x73: { code: 'F4' }, // [USB: 0x3d] 207 | 0x74: { code: 'F5' }, // [USB: 0x3e] 208 | 0x75: { code: 'F6' }, // [USB: 0x3f] 209 | 0x76: { code: 'F7' }, // [USB: 0x40] 210 | 0x77: { code: 'F8' }, // [USB: 0x41] 211 | 0x78: { code: 'F9' }, // [USB: 0x42] 212 | 0x79: { code: 'F10' }, // [USB: 0x43] 213 | 0x7A: { code: 'F11' }, // [USB: 0x44] 214 | 0x7B: { code: 'F12' }, // [USB: 0x45] 215 | 0x7C: { code: 'F13' }, // [USB: 0x68] 216 | 0x7D: { code: 'F14' }, // [USB: 0x69] 217 | 0x7E: { code: 'F15' }, // [USB: 0x6a] 218 | 0x7F: { code: 'F16' }, // [USB: 0x6b] 219 | 220 | 0x80: { code: 'F17' }, // [USB: 0x6c] 221 | 0x81: { code: 'F18' }, // [USB: 0x6d] 222 | 0x82: { code: 'F19' }, // [USB: 0x6e] 223 | 0x83: { code: 'F20' }, // [USB: 0x6f] 224 | 0x84: { code: 'F21' }, // [USB: 0x70] 225 | 0x85: { code: 'F22' }, // [USB: 0x71] 226 | 0x86: { code: 'F23' }, // [USB: 0x72] 227 | 0x87: { code: 'F24' }, // [USB: 0x73] 228 | // 0x88-0x8F - unassigned 229 | 230 | 0x90: { code: 'NumLock', location: NUMPAD }, // [USB: 0x53] 231 | 0x91: { code: 'ScrollLock' }, // [USB: 0x47] 232 | // 0x92-0x96 - OEM specific 233 | // 0x97-0x9F - unassigned 234 | 235 | // NOTE: 0xA0-0xA5 usually mapped to 0x10-0x12 in browsers 236 | 0xA0: { code: 'ShiftLeft', location: LEFT }, // [USB: 0xe1] 237 | 0xA1: { code: 'ShiftRight', location: RIGHT }, // [USB: 0xe5] 238 | 0xA2: { code: 'ControlLeft', location: LEFT }, // [USB: 0xe0] 239 | 0xA3: { code: 'ControlRight', location: RIGHT }, // [USB: 0xe4] 240 | 0xA4: { code: 'AltLeft', location: LEFT }, // [USB: 0xe2] 241 | 0xA5: { code: 'AltRight', location: RIGHT }, // [USB: 0xe6] 242 | 243 | 0xA6: { code: 'BrowserBack' }, // [USB: 0x0c/0x0224] 244 | 0xA7: { code: 'BrowserForward' }, // [USB: 0x0c/0x0225] 245 | 0xA8: { code: 'BrowserRefresh' }, // [USB: 0x0c/0x0227] 246 | 0xA9: { code: 'BrowserStop' }, // [USB: 0x0c/0x0226] 247 | 0xAA: { code: 'BrowserSearch' }, // [USB: 0x0c/0x0221] 248 | 0xAB: { code: 'BrowserFavorites' }, // [USB: 0x0c/0x0228] 249 | 0xAC: { code: 'BrowserHome' }, // [USB: 0x0c/0x0222] 250 | 0xAD: { code: 'VolumeMute' }, // [USB: 0x7f] 251 | 0xAE: { code: 'VolumeDown' }, // [USB: 0x81] 252 | 0xAF: { code: 'VolumeUp' }, // [USB: 0x80] 253 | 254 | 0xB0: { code: 'MediaTrackNext' }, // [USB: 0x0c/0x00b5] 255 | 0xB1: { code: 'MediaTrackPrevious' }, // [USB: 0x0c/0x00b6] 256 | 0xB2: { code: 'MediaStop' }, // [USB: 0x0c/0x00b7] 257 | 0xB3: { code: 'MediaPlayPause' }, // [USB: 0x0c/0x00cd] 258 | 0xB4: { code: 'LaunchMail' }, // [USB: 0x0c/0x018a] 259 | 0xB5: { code: 'MediaSelect' }, 260 | 0xB6: { code: 'LaunchApp1' }, 261 | 0xB7: { code: 'LaunchApp2' }, 262 | // 0xB8-0xB9 - reserved 263 | 0xBA: { code: 'Semicolon', keyCap: ';' }, // [USB: 0x33] ;: (US Standard 101) 264 | 0xBB: { code: 'Equal', keyCap: '=' }, // [USB: 0x2e] =+ 265 | 0xBC: { code: 'Comma', keyCap: ',' }, // [USB: 0x36] ,< 266 | 0xBD: { code: 'Minus', keyCap: '-' }, // [USB: 0x2d] -_ 267 | 0xBE: { code: 'Period', keyCap: '.' }, // [USB: 0x37] .> 268 | 0xBF: { code: 'Slash', keyCap: '/' }, // [USB: 0x38] /? (US Standard 101) 269 | 270 | 0xC0: { code: 'Backquote', keyCap: '`' }, // [USB: 0x35] `~ (US Standard 101) 271 | // 0xC1-0xCF - reserved 272 | 273 | // 0xD0-0xD7 - reserved 274 | // 0xD8-0xDA - unassigned 275 | 0xDB: { code: 'BracketLeft', keyCap: '[' }, // [USB: 0x2f] [{ (US Standard 101) 276 | 0xDC: { code: 'Backslash', keyCap: '\\' }, // [USB: 0x31] \| (US Standard 101) 277 | 0xDD: { code: 'BracketRight', keyCap: ']' }, // [USB: 0x30] ]} (US Standard 101) 278 | 0xDE: { code: 'Quote', keyCap: '\'' }, // [USB: 0x34] '" (US Standard 101) 279 | // 0xDF - miscellaneous/varies 280 | 281 | // 0xE0 - reserved 282 | // 0xE1 - OEM specific 283 | 0xE2: { code: 'IntlBackslash', keyCap: '\\' }, // [USB: 0x64] \| (UK Standard 102) 284 | // 0xE3-0xE4 - OEM specific 285 | 0xE5: { code: 'Process' }, // (Not in D3E) 286 | // 0xE6 - OEM specific 287 | // 0xE7 - VK_PACKET 288 | // 0xE8 - unassigned 289 | // 0xE9-0xEF - OEM specific 290 | 291 | // 0xF0-0xF5 - OEM specific 292 | 0xF6: { code: 'Attn' }, // [USB: 0x9a] (Not in D3E) 293 | 0xF7: { code: 'CrSel' }, // [USB: 0xa3] (Not in D3E) 294 | 0xF8: { code: 'ExSel' }, // [USB: 0xa4] (Not in D3E) 295 | 0xF9: { code: 'EraseEof' }, // (Not in D3E) 296 | 0xFA: { code: 'Play' }, // (Not in D3E) 297 | 0xFB: { code: 'ZoomToggle' }, // (Not in D3E) 298 | // 0xFC - VK_NONAME - reserved 299 | // 0xFD - VK_PA1 300 | 0xFE: { code: 'Clear' } // [USB: 0x9c] (Not in D3E) 301 | }; 302 | 303 | // No legacy keyCode, but listed in D3E: 304 | 305 | // code: usb 306 | // 'IntlHash': 0x070032, 307 | // 'IntlRo': 0x070087, 308 | // 'IntlYen': 0x070089, 309 | // 'NumpadBackspace': 0x0700bb, 310 | // 'NumpadClear': 0x0700d8, 311 | // 'NumpadClearEntry': 0x0700d9, 312 | // 'NumpadMemoryAdd': 0x0700d3, 313 | // 'NumpadMemoryClear': 0x0700d2, 314 | // 'NumpadMemoryRecall': 0x0700d1, 315 | // 'NumpadMemoryStore': 0x0700d0, 316 | // 'NumpadMemorySubtract': 0x0700d4, 317 | // 'NumpadParenLeft': 0x0700b6, 318 | // 'NumpadParenRight': 0x0700b7, 319 | 320 | //-------------------------------------------------------------------- 321 | // 322 | // Browser/OS Specific Mappings 323 | // 324 | //-------------------------------------------------------------------- 325 | 326 | mergeIf(keyCodeToInfoTable, 327 | 'moz', { 328 | 0x3B: { code: 'Semicolon', keyCap: ';' }, // [USB: 0x33] ;: (US Standard 101) 329 | 0x3D: { code: 'Equal', keyCap: '=' }, // [USB: 0x2e] =+ 330 | 0x6B: { code: 'Equal', keyCap: '=' }, // [USB: 0x2e] =+ 331 | 0x6D: { code: 'Minus', keyCap: '-' }, // [USB: 0x2d] -_ 332 | 0xBB: { code: 'NumpadAdd', keyCap: '+', location: NUMPAD }, // [USB: 0x57] 333 | 0xBD: { code: 'NumpadSubtract', keyCap: '-', location: NUMPAD } // [USB: 0x56] 334 | }); 335 | 336 | mergeIf(keyCodeToInfoTable, 337 | 'moz-mac', { 338 | 0x0C: { code: 'NumLock', location: NUMPAD }, // [USB: 0x53] 339 | 0xAD: { code: 'Minus', keyCap: '-' } // [USB: 0x2d] -_ 340 | }); 341 | 342 | mergeIf(keyCodeToInfoTable, 343 | 'moz-win', { 344 | 0xAD: { code: 'Minus', keyCap: '-' } // [USB: 0x2d] -_ 345 | }); 346 | 347 | mergeIf(keyCodeToInfoTable, 348 | 'chrome-mac', { 349 | 0x5D: { code: 'OSRight', location: RIGHT } // [USB: 0xe7] 350 | }); 351 | 352 | // Windows via Bootcamp (!) 353 | if (0) { 354 | mergeIf(keyCodeToInfoTable, 355 | 'chrome-win', { 356 | 0xC0: { code: 'Quote', keyCap: '\'' }, // [USB: 0x34] '" (US Standard 101) 357 | 0xDE: { code: 'Backslash', keyCap: '\\' }, // [USB: 0x31] \| (US Standard 101) 358 | 0xDF: { code: 'Backquote', keyCap: '`' } // [USB: 0x35] `~ (US Standard 101) 359 | }); 360 | 361 | mergeIf(keyCodeToInfoTable, 362 | 'ie', { 363 | 0xC0: { code: 'Quote', keyCap: '\'' }, // [USB: 0x34] '" (US Standard 101) 364 | 0xDE: { code: 'Backslash', keyCap: '\\' }, // [USB: 0x31] \| (US Standard 101) 365 | 0xDF: { code: 'Backquote', keyCap: '`' } // [USB: 0x35] `~ (US Standard 101) 366 | }); 367 | } 368 | 369 | mergeIf(keyCodeToInfoTable, 370 | 'safari', { 371 | 0x03: { code: 'Enter' }, // [USB: 0x28] old Safari 372 | 0x19: { code: 'Tab' } // [USB: 0x2b] old Safari for Shift+Tab 373 | }); 374 | 375 | mergeIf(keyCodeToInfoTable, 376 | 'ios', { 377 | 0x0A: { code: 'Enter', location: STANDARD } // [USB: 0x28] 378 | }); 379 | 380 | mergeIf(keyCodeToInfoTable, 381 | 'safari-mac', { 382 | 0x5B: { code: 'OSLeft', location: LEFT }, // [USB: 0xe3] 383 | 0x5D: { code: 'OSRight', location: RIGHT }, // [USB: 0xe7] 384 | 0xE5: { code: 'KeyQ', keyCap: 'Q' } // [USB: 0x14] On alternate presses, Ctrl+Q sends this 385 | }); 386 | 387 | //-------------------------------------------------------------------- 388 | // 389 | // Identifier Mappings 390 | // 391 | //-------------------------------------------------------------------- 392 | 393 | // Cases where newer-ish browsers send keyIdentifier which can be 394 | // used to disambiguate keys. 395 | 396 | // keyIdentifierTable[keyIdentifier] -> keyInfo 397 | 398 | var keyIdentifierTable = {}; 399 | if ('cros' === os) { 400 | keyIdentifierTable['U+00A0'] = { code: 'ShiftLeft', location: LEFT }; 401 | keyIdentifierTable['U+00A1'] = { code: 'ShiftRight', location: RIGHT }; 402 | keyIdentifierTable['U+00A2'] = { code: 'ControlLeft', location: LEFT }; 403 | keyIdentifierTable['U+00A3'] = { code: 'ControlRight', location: RIGHT }; 404 | keyIdentifierTable['U+00A4'] = { code: 'AltLeft', location: LEFT }; 405 | keyIdentifierTable['U+00A5'] = { code: 'AltRight', location: RIGHT }; 406 | } 407 | if ('chrome-mac' === browser_os) { 408 | keyIdentifierTable['U+0010'] = { code: 'ContextMenu' }; 409 | } 410 | if ('safari-mac' === browser_os) { 411 | keyIdentifierTable['U+0010'] = { code: 'ContextMenu' }; 412 | } 413 | if ('ios' === os) { 414 | // These only generate keyup events 415 | keyIdentifierTable['U+0010'] = { code: 'Function' }; 416 | 417 | keyIdentifierTable['U+001C'] = { code: 'ArrowLeft' }; 418 | keyIdentifierTable['U+001D'] = { code: 'ArrowRight' }; 419 | keyIdentifierTable['U+001E'] = { code: 'ArrowUp' }; 420 | keyIdentifierTable['U+001F'] = { code: 'ArrowDown' }; 421 | 422 | keyIdentifierTable['U+0001'] = { code: 'Home' }; // [USB: 0x4a] Fn + ArrowLeft 423 | keyIdentifierTable['U+0004'] = { code: 'End' }; // [USB: 0x4d] Fn + ArrowRight 424 | keyIdentifierTable['U+000B'] = { code: 'PageUp' }; // [USB: 0x4b] Fn + ArrowUp 425 | keyIdentifierTable['U+000C'] = { code: 'PageDown' }; // [USB: 0x4e] Fn + ArrowDown 426 | } 427 | 428 | //-------------------------------------------------------------------- 429 | // 430 | // Location Mappings 431 | // 432 | //-------------------------------------------------------------------- 433 | 434 | // Cases where newer-ish browsers send location/keyLocation which 435 | // can be used to disambiguate keys. 436 | 437 | // locationTable[location][keyCode] -> keyInfo 438 | var locationTable = []; 439 | locationTable[LEFT] = { 440 | 0x10: { code: 'ShiftLeft', location: LEFT }, // [USB: 0xe1] 441 | 0x11: { code: 'ControlLeft', location: LEFT }, // [USB: 0xe0] 442 | 0x12: { code: 'AltLeft', location: LEFT } // [USB: 0xe2] 443 | }; 444 | locationTable[RIGHT] = { 445 | 0x10: { code: 'ShiftRight', location: RIGHT }, // [USB: 0xe5] 446 | 0x11: { code: 'ControlRight', location: RIGHT }, // [USB: 0xe4] 447 | 0x12: { code: 'AltRight', location: RIGHT } // [USB: 0xe6] 448 | }; 449 | locationTable[NUMPAD] = { 450 | 0x0D: { code: 'NumpadEnter', location: NUMPAD } // [USB: 0x58] 451 | }; 452 | 453 | mergeIf(locationTable[NUMPAD], 'moz', { 454 | 0x6D: { code: 'NumpadSubtract', location: NUMPAD }, // [USB: 0x56] 455 | 0x6B: { code: 'NumpadAdd', location: NUMPAD } // [USB: 0x57] 456 | }); 457 | mergeIf(locationTable[LEFT], 'moz-mac', { 458 | 0xE0: { code: 'OSLeft', location: LEFT } // [USB: 0xe3] 459 | }); 460 | mergeIf(locationTable[RIGHT], 'moz-mac', { 461 | 0xE0: { code: 'OSRight', location: RIGHT } // [USB: 0xe7] 462 | }); 463 | mergeIf(locationTable[RIGHT], 'moz-win', { 464 | 0x5B: { code: 'OSRight', location: RIGHT } // [USB: 0xe7] 465 | }); 466 | 467 | 468 | mergeIf(locationTable[RIGHT], 'mac', { 469 | 0x5D: { code: 'OSRight', location: RIGHT } // [USB: 0xe7] 470 | }); 471 | 472 | mergeIf(locationTable[NUMPAD], 'chrome-mac', { 473 | 0x0C: { code: 'NumLock', location: NUMPAD } // [USB: 0x53] 474 | }); 475 | 476 | mergeIf(locationTable[NUMPAD], 'safari-mac', { 477 | 0x0C: { code: 'NumLock', location: NUMPAD }, // [USB: 0x53] 478 | 0xBB: { code: 'NumpadAdd', location: NUMPAD }, // [USB: 0x57] 479 | 0xBD: { code: 'NumpadSubtract', location: NUMPAD }, // [USB: 0x56] 480 | 0xBE: { code: 'NumpadDecimal', location: NUMPAD }, // [USB: 0x63] 481 | 0xBF: { code: 'NumpadDivide', location: NUMPAD } // [USB: 0x54] 482 | }); 483 | 484 | 485 | //-------------------------------------------------------------------- 486 | // 487 | // Key Values 488 | // 489 | //-------------------------------------------------------------------- 490 | 491 | // Mapping from `code` values to `key` values. Values defined at: 492 | // https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3Events-key.html 493 | // Entries are only provided when `key` differs from `code`. If 494 | // printable, `shiftKey` has the shifted printable character. This 495 | // assumes US Standard 101 layout 496 | 497 | var codeToKeyTable = { 498 | // Modifier Keys 499 | ShiftLeft: { key: 'Shift' }, 500 | ShiftRight: { key: 'Shift' }, 501 | ControlLeft: { key: 'Control' }, 502 | ControlRight: { key: 'Control' }, 503 | AltLeft: { key: 'Alt' }, 504 | AltRight: { key: 'Alt' }, 505 | OSLeft: { key: 'OS' }, 506 | OSRight: { key: 'OS' }, 507 | 508 | // Whitespace Keys 509 | NumpadEnter: { key: 'Enter' }, 510 | Space: { key: ' ' }, 511 | 512 | // Printable Keys 513 | Digit0: { key: '0', shiftKey: ')' }, 514 | Digit1: { key: '1', shiftKey: '!' }, 515 | Digit2: { key: '2', shiftKey: '@' }, 516 | Digit3: { key: '3', shiftKey: '#' }, 517 | Digit4: { key: '4', shiftKey: '$' }, 518 | Digit5: { key: '5', shiftKey: '%' }, 519 | Digit6: { key: '6', shiftKey: '^' }, 520 | Digit7: { key: '7', shiftKey: '&' }, 521 | Digit8: { key: '8', shiftKey: '*' }, 522 | Digit9: { key: '9', shiftKey: '(' }, 523 | KeyA: { key: 'a', shiftKey: 'A' }, 524 | KeyB: { key: 'b', shiftKey: 'B' }, 525 | KeyC: { key: 'c', shiftKey: 'C' }, 526 | KeyD: { key: 'd', shiftKey: 'D' }, 527 | KeyE: { key: 'e', shiftKey: 'E' }, 528 | KeyF: { key: 'f', shiftKey: 'F' }, 529 | KeyG: { key: 'g', shiftKey: 'G' }, 530 | KeyH: { key: 'h', shiftKey: 'H' }, 531 | KeyI: { key: 'i', shiftKey: 'I' }, 532 | KeyJ: { key: 'j', shiftKey: 'J' }, 533 | KeyK: { key: 'k', shiftKey: 'K' }, 534 | KeyL: { key: 'l', shiftKey: 'L' }, 535 | KeyM: { key: 'm', shiftKey: 'M' }, 536 | KeyN: { key: 'n', shiftKey: 'N' }, 537 | KeyO: { key: 'o', shiftKey: 'O' }, 538 | KeyP: { key: 'p', shiftKey: 'P' }, 539 | KeyQ: { key: 'q', shiftKey: 'Q' }, 540 | KeyR: { key: 'r', shiftKey: 'R' }, 541 | KeyS: { key: 's', shiftKey: 'S' }, 542 | KeyT: { key: 't', shiftKey: 'T' }, 543 | KeyU: { key: 'u', shiftKey: 'U' }, 544 | KeyV: { key: 'v', shiftKey: 'V' }, 545 | KeyW: { key: 'w', shiftKey: 'W' }, 546 | KeyX: { key: 'x', shiftKey: 'X' }, 547 | KeyY: { key: 'y', shiftKey: 'Y' }, 548 | KeyZ: { key: 'z', shiftKey: 'Z' }, 549 | Numpad0: { key: '0' }, 550 | Numpad1: { key: '1' }, 551 | Numpad2: { key: '2' }, 552 | Numpad3: { key: '3' }, 553 | Numpad4: { key: '4' }, 554 | Numpad5: { key: '5' }, 555 | Numpad6: { key: '6' }, 556 | Numpad7: { key: '7' }, 557 | Numpad8: { key: '8' }, 558 | Numpad9: { key: '9' }, 559 | NumpadMultiply: { key: '*' }, 560 | NumpadAdd: { key: '+' }, 561 | NumpadComma: { key: ',' }, 562 | NumpadSubtract: { key: '-' }, 563 | NumpadDecimal: { key: '.' }, 564 | NumpadDivide: { key: '/' }, 565 | Semicolon: { key: ';', shiftKey: ':' }, 566 | Equal: { key: '=', shiftKey: '+' }, 567 | Comma: { key: ',', shiftKey: '<' }, 568 | Minus: { key: '-', shiftKey: '_' }, 569 | Period: { key: '.', shiftKey: '>' }, 570 | Slash: { key: '/', shiftKey: '?' }, 571 | Backquote: { key: '`', shiftKey: '~' }, 572 | BracketLeft: { key: '[', shiftKey: '{' }, 573 | Backslash: { key: '\\', shiftKey: '|' }, 574 | BracketRight: { key: ']', shiftKey: '}' }, 575 | Quote: { key: '\'', shiftKey: '"' }, 576 | IntlBackslash: { key: '\\', shiftKey: '|' } 577 | }; 578 | 579 | mergeIf(codeToKeyTable, 'mac', { 580 | OSLeft: { key: 'Meta' }, 581 | OSRight: { key: 'Meta' } 582 | }); 583 | 584 | // Corrections for 'key' names in older browsers (e.g. FF36-) 585 | // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.key#Key_values 586 | var keyFixTable = { 587 | Esc: 'Escape', 588 | Nonconvert: 'NonConvert', 589 | Left: 'ArrowLeft', 590 | Up: 'ArrowUp', 591 | Right: 'ArrowRight', 592 | Down: 'ArrowDown', 593 | Del: 'Delete', 594 | Menu: 'ContextMenu', 595 | MediaNextTrack: 'MediaTrackNext', 596 | MediaPreviousTrack: 'MediaTrackPrevious', 597 | SelectMedia: 'MediaSelect', 598 | HalfWidth: 'Hankaku', 599 | FullWidth: 'Zenkaku', 600 | RomanCharacters: 'Romaji', 601 | Crsel: 'CrSel', 602 | Exsel: 'ExSel', 603 | Zoom: 'ZoomToggle' 604 | }; 605 | 606 | //-------------------------------------------------------------------- 607 | // 608 | // Exported Functions 609 | // 610 | //-------------------------------------------------------------------- 611 | 612 | 613 | var codeTable = remap(keyCodeToInfoTable, 'code'); 614 | 615 | try { 616 | var nativeLocation = nativeKeyboardEvent && ('location' in new KeyboardEvent('')); 617 | } catch (_) {} 618 | 619 | function keyInfoForEvent(event) { 620 | var keyCode = 'keyCode' in event ? event.keyCode : 'which' in event ? event.which : 0; 621 | 622 | var keyInfo = (function(){ 623 | if (nativeLocation || 'keyLocation' in event) { 624 | var location = nativeLocation ? event.location : event.keyLocation; 625 | if (location && keyCode in locationTable[location]) { 626 | return locationTable[location][keyCode]; 627 | } 628 | } 629 | if ('keyIdentifier' in event && event.keyIdentifier in keyIdentifierTable) { 630 | return keyIdentifierTable[event.keyIdentifier]; 631 | } 632 | if (keyCode in keyCodeToInfoTable) { 633 | return keyCodeToInfoTable[keyCode]; 634 | } 635 | return null; 636 | }()); 637 | 638 | // TODO: Track these down and move to general tables 639 | if (0) { 640 | // TODO: Map these for newerish browsers? 641 | // TODO: iOS only? 642 | // TODO: Override with more common keyIdentifier name? 643 | switch (event.keyIdentifier) { 644 | case 'U+0010': keyInfo = { code: 'Function' }; break; 645 | case 'U+001C': keyInfo = { code: 'ArrowLeft' }; break; 646 | case 'U+001D': keyInfo = { code: 'ArrowRight' }; break; 647 | case 'U+001E': keyInfo = { code: 'ArrowUp' }; break; 648 | case 'U+001F': keyInfo = { code: 'ArrowDown' }; break; 649 | } 650 | } 651 | 652 | if (!keyInfo) 653 | return null; 654 | 655 | var key = (function() { 656 | var entry = codeToKeyTable[keyInfo.code]; 657 | if (!entry) return keyInfo.code; 658 | return (event.shiftKey && 'shiftKey' in entry) ? entry.shiftKey : entry.key; 659 | }()); 660 | 661 | return { 662 | code: keyInfo.code, 663 | key: key, 664 | location: keyInfo.location, 665 | keyCap: keyInfo.keyCap 666 | }; 667 | } 668 | 669 | function queryKeyCap(code, locale) { 670 | code = String(code); 671 | if (!codeTable.hasOwnProperty(code)) return 'Undefined'; 672 | if (locale && String(locale).toLowerCase() !== 'en-us') throw Error('Unsupported locale'); 673 | var keyInfo = codeTable[code]; 674 | return keyInfo.keyCap || keyInfo.code || 'Undefined'; 675 | } 676 | 677 | if ('KeyboardEvent' in global && 'defineProperty' in Object) { 678 | (function() { 679 | function define(o, p, v) { 680 | if (p in o) return; 681 | Object.defineProperty(o, p, v); 682 | } 683 | 684 | define(KeyboardEvent.prototype, 'code', { get: function() { 685 | var keyInfo = keyInfoForEvent(this); 686 | return keyInfo ? keyInfo.code : ''; 687 | }}); 688 | 689 | // Fix for nonstandard `key` values (FF36-) 690 | if ('key' in KeyboardEvent.prototype) { 691 | var desc = Object.getOwnPropertyDescriptor(KeyboardEvent.prototype, 'key'); 692 | Object.defineProperty(KeyboardEvent.prototype, 'key', { get: function() { 693 | var key = desc.get.call(this); 694 | return keyFixTable.hasOwnProperty(key) ? keyFixTable[key] : key; 695 | }}); 696 | } 697 | 698 | define(KeyboardEvent.prototype, 'key', { get: function() { 699 | var keyInfo = keyInfoForEvent(this); 700 | return (keyInfo && 'key' in keyInfo) ? keyInfo.key : 'Unidentified'; 701 | }}); 702 | 703 | define(KeyboardEvent.prototype, 'location', { get: function() { 704 | var keyInfo = keyInfoForEvent(this); 705 | return (keyInfo && 'location' in keyInfo) ? keyInfo.location : STANDARD; 706 | }}); 707 | 708 | define(KeyboardEvent.prototype, 'locale', { get: function() { 709 | return ''; 710 | }}); 711 | }()); 712 | } 713 | 714 | if (!('queryKeyCap' in global.KeyboardEvent)) 715 | global.KeyboardEvent.queryKeyCap = queryKeyCap; 716 | 717 | // Helper for IE8- 718 | global.identifyKey = function(event) { 719 | if ('code' in event) 720 | return; 721 | 722 | var keyInfo = keyInfoForEvent(event); 723 | event.code = keyInfo ? keyInfo.code : ''; 724 | event.key = (keyInfo && 'key' in keyInfo) ? keyInfo.key : 'Unidentified'; 725 | event.location = ('location' in event) ? event.location : 726 | ('keyLocation' in event) ? event.keyLocation : 727 | (keyInfo && 'location' in keyInfo) ? keyInfo.location : STANDARD; 728 | event.locale = ''; 729 | }; 730 | 731 | } (window)); 732 | -------------------------------------------------------------------------------- /client/demo/assets/tree2.dae: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CINEMA4D 15.064 COLLADA Exporter 6 | 7 | 2015-08-28T05:43:48Z 8 | 2015-08-28T05:43:48Z 9 | 10 | Y_UP 11 | 12 | 13 | 14 | tex/shadow-circle.png 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 0.8 0.8 0.8 1 24 | 25 | 26 | 0.2 0.2 0.2 1 27 | 28 | 29 | 0.5 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 0.0627451 0.396078 0.513725 1 41 | 42 | 43 | 0.2 0.2 0.2 1 44 | 45 | 46 | 0.5 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 0.0235294 0.564706 0.607843 1 58 | 59 | 60 | 0.2 0.2 0.2 1 61 | 62 | 63 | 0.5 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 0.164706 0.72549 0.701961 1 75 | 76 | 77 | 0.2 0.2 0.2 1 78 | 79 | 80 | 0.5 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 0.654902 0.47451 0.415686 1 92 | 93 | 94 | 0.2 0.2 0.2 1 95 | 96 | 97 | 0.5 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | ID13 108 | 109 | 110 | 111 | 112 | ID14 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 1 1 1 1 122 | 123 | 124 | 0.88 125 | 126 | 127 | 1 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -32.0367 -60 32.0367 32.0367 -60 32.0367 32.0367 -60 -32.0367 -32.0367 -60 -32.0367 -32.0367 -57.6676 37.6676 32.0367 -57.6676 37.6676 36.0183 -57.6676 36.0183 37.6676 -57.6676 32.0367 37.6676 -57.6676 -32.0367 36.0183 -57.6676 -36.0183 32.0367 -57.6676 -37.6676 -32.0367 -57.6676 -37.6676 -36.0183 -57.6676 -36.0183 -37.6676 -57.6676 -32.0367 -37.6676 -57.6676 32.0367 -36.0183 -57.6676 36.0183 -32.0367 -52.0367 40 32.0367 -52.0367 40 37.6676 -52.0367 37.6676 40 -52.0367 32.0367 40 -52.0367 -32.0367 37.6676 -52.0367 -37.6676 32.0367 -52.0367 -40 -32.0367 -52.0367 -40 -37.6676 -52.0367 -37.6676 -40 -52.0367 -32.0367 -40 -52.0367 32.0367 -37.6676 -52.0367 37.6676 -32.0367 52.0367 40 32.0367 52.0367 40 37.6676 52.0367 37.6676 40 52.0367 32.0367 40 52.0367 -32.0367 37.6676 52.0367 -37.6676 32.0367 52.0367 -40 -32.0367 52.0367 -40 -37.6676 52.0367 -37.6676 -40 52.0367 -32.0367 -40 52.0367 32.0367 -37.6676 52.0367 37.6676 -32.0367 57.6676 37.6676 32.0367 57.6676 37.6676 36.0183 57.6676 36.0183 37.6676 57.6676 32.0367 37.6676 57.6676 -32.0367 36.0183 57.6676 -36.0183 32.0367 57.6676 -37.6676 -32.0367 57.6676 -37.6676 -36.0183 57.6676 -36.0183 -37.6676 57.6676 -32.0367 -37.6676 57.6676 32.0367 -36.0183 57.6676 36.0183 -32.0367 60 32.0367 32.0367 60 32.0367 32.0367 60 -32.0367 -32.0367 60 -32.0367 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -0.0785213 -0.921027 0.381502 0.0785213 -0.921027 0.381502 0 -0.382683 0.92388 0 0 1 0 0.382683 0.92388 -0.0785213 0.921027 0.381502 0.0785213 0.921027 0.381502 0.156558 -0.912487 0.377964 0.357407 -0.357407 0.862856 0.382683 0 0.92388 0.357406 0.357407 0.862856 0.156558 0.912487 0.377964 0.381502 -0.921027 0.0785213 0.377964 -0.912487 0.156558 0.862856 -0.357407 0.357407 0.92388 0 0.382683 0.862856 0.357407 0.357407 0.377964 0.912487 0.156558 0.381502 0.921027 0.0785213 0.381502 -0.921027 -0.0785213 0.92388 -0.382683 -0 1 0 -0 0.92388 0.382683 -0 0.381502 0.921027 -0.0785213 0.377964 -0.912487 -0.156558 0.862856 -0.357407 -0.357407 0.92388 0 -0.382683 0.862856 0.357407 -0.357407 0.377964 0.912487 -0.156558 0.0785213 -0.921027 -0.381502 0.156558 -0.912487 -0.377964 0.357406 -0.357407 -0.862856 0.382683 0 -0.92388 0.357407 0.357407 -0.862856 0.156558 0.912487 -0.377964 0.0785213 0.921027 -0.381502 -0.0785213 -0.921027 -0.381502 0 -0.382683 -0.92388 0 0 -1 0 0.382683 -0.92388 -0.0785213 0.921027 -0.381502 -0.156558 -0.912487 -0.377964 -0.357407 -0.357407 -0.862856 -0.382683 0 -0.92388 -0.357406 0.357407 -0.862856 -0.156558 0.912487 -0.377964 -0.381502 -0.921027 -0.0785213 -0.377964 -0.912487 -0.156558 -0.862856 -0.357407 -0.357407 -0.92388 0 -0.382683 -0.862856 0.357407 -0.357407 -0.377964 0.912487 -0.156558 -0.381502 0.921027 -0.0785213 -0.381502 -0.921027 0.0785213 -0.92388 -0.382683 -0 -1 0 -0 -0.92388 0.382683 -0 -0.381502 0.921027 0.0785213 -0.377964 -0.912487 0.156558 -0.862856 -0.357407 0.357407 -0.92388 0 0.382683 -0.862856 0.357407 0.357407 -0.377964 0.912487 0.156558 -0.156558 -0.912487 0.377964 -0.357406 -0.357407 0.862856 -0.382683 0 0.92388 -0.357407 0.357407 0.862856 -0.156558 0.912487 0.377964 0 1 -0 0 -1 -0 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 0 0 0 0.0484494 0.836663 0.0484494 0.836663 0 0 0.0968987 0.836663 0.0968987 0 0.903101 0.836663 0.903101 0 0.951551 0.836663 0.951551 0 1 0.836663 1 0.877497 0 0.918331 0.0484494 0.918331 0.0968987 0.918331 0.903101 0.918331 0.951551 0.877497 1 0.959166 0 1 0.0484494 1 0.0968987 1 0.903101 1 0.951551 0.959166 1 1 1 1 0 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 194 |

1 1 3 5 1 2 4 0 1 0 0 0 5 2 2 17 2 5 16 2 4 4 2 1 17 3 5 29 3 7 28 3 6 16 3 4 29 4 7 41 4 9 40 4 8 28 4 6 41 6 9 53 6 11 52 5 10 40 5 8 6 7 13 5 1 2 1 1 12 6 8 13 18 8 14 17 8 5 5 8 2 18 9 14 30 9 15 29 9 7 17 9 5 30 10 15 42 10 16 41 10 9 29 10 7 42 11 16 53 6 17 41 6 9 7 12 19 6 13 13 1 12 18 7 14 19 19 14 20 18 14 14 6 14 13 19 15 20 31 15 21 30 15 15 18 15 14 31 16 21 43 16 22 42 16 16 30 16 15 43 18 22 53 18 23 42 17 16 2 19 3 8 19 2 7 12 1 1 12 0 8 20 2 20 20 5 19 20 4 7 20 1 20 21 5 32 21 7 31 21 6 19 21 4 32 22 7 44 22 9 43 22 8 31 22 6 44 23 9 54 23 11 53 18 10 43 18 8 9 24 13 8 19 2 2 19 12 9 25 13 21 25 14 20 25 5 8 25 2 21 26 14 33 26 15 32 26 7 20 26 5 33 27 15 45 27 16 44 27 9 32 27 7 45 28 16 54 23 17 44 23 9 10 29 19 9 30 13 2 29 18 10 31 19 22 31 20 21 31 14 9 31 13 22 32 20 34 32 21 33 32 15 21 32 14 34 33 21 46 33 22 45 33 16 33 33 15 46 35 22 54 35 23 45 34 16 3 36 3 11 36 2 10 29 1 2 29 0 11 37 2 23 37 5 22 37 4 10 37 1 23 38 5 35 38 7 34 38 6 22 38 4 35 39 7 47 39 9 46 39 8 34 39 6 47 40 9 55 40 11 54 35 10 46 35 8 12 41 13 11 36 2 3 36 12 12 42 13 24 42 14 23 42 5 11 42 2 24 43 14 36 43 15 35 43 7 23 43 5 36 44 15 48 44 16 47 44 9 35 44 7 48 45 16 55 40 17 47 40 9 13 46 19 12 47 13 3 46 18 13 48 19 25 48 20 24 48 14 12 48 13 25 49 20 37 49 21 36 49 15 24 49 14 37 50 21 49 50 22 48 50 16 36 50 15 49 52 22 55 52 23 48 51 16 0 53 3 14 53 2 13 46 1 3 46 0 14 54 2 26 54 5 25 54 4 13 54 1 26 55 5 38 55 7 37 55 6 25 55 4 38 56 7 50 56 9 49 56 8 37 56 6 50 57 9 52 57 11 55 52 10 49 52 8 15 58 13 14 53 2 0 53 12 15 59 13 27 59 14 26 59 5 14 59 2 27 60 14 39 60 15 38 60 7 26 60 5 39 61 15 51 61 16 50 61 9 38 61 7 51 62 16 52 57 17 50 57 9 4 0 19 15 63 13 0 0 18 4 64 19 16 64 20 27 64 14 15 64 13 16 65 20 28 65 21 39 65 15 27 65 14 28 66 21 40 66 22 51 66 16 39 66 15 40 5 22 52 5 23 51 67 16 53 68 25 54 68 24 55 68 10 52 68 0 3 69 0 2 69 25 1 69 24 0 69 10

195 |
196 |
197 |
198 | 199 | 200 | 201 | -18.4211 -34.5 62.1092 18.4211 -34.5 62.1092 18.4211 -34.5 -62.1092 -18.4211 -34.5 -62.1092 -18.4211 -33.1589 65.3469 18.4211 -33.1589 65.3469 20.7105 -33.1589 64.3986 21.6589 -33.1589 62.1092 21.6589 -33.1589 -62.1092 20.7105 -33.1589 -64.3986 18.4211 -33.1589 -65.3469 -18.4211 -33.1589 -65.3469 -20.7105 -33.1589 -64.3986 -21.6589 -33.1589 -62.1092 -21.6589 -33.1589 62.1092 -20.7105 -33.1589 64.3986 -18.4211 -29.9211 66.6881 18.4211 -29.9211 66.6881 21.6589 -29.9211 65.3469 23 -29.9211 62.1092 23 -29.9211 -62.1092 21.6589 -29.9211 -65.3469 18.4211 -29.9211 -66.6881 -18.4211 -29.9211 -66.6881 -21.6589 -29.9211 -65.3469 -23 -29.9211 -62.1092 -23 -29.9211 62.1092 -21.6589 -29.9211 65.3469 -18.4211 29.9211 66.6881 18.4211 29.9211 66.6881 21.6589 29.9211 65.3469 23 29.9211 62.1092 23 29.9211 -62.1092 21.6589 29.9211 -65.3469 18.4211 29.9211 -66.6881 -18.4211 29.9211 -66.6881 -21.6589 29.9211 -65.3469 -23 29.9211 -62.1092 -23 29.9211 62.1092 -21.6589 29.9211 65.3469 -18.4211 33.1589 65.3469 18.4211 33.1589 65.3469 20.7105 33.1589 64.3986 21.6589 33.1589 62.1092 21.6589 33.1589 -62.1092 20.7105 33.1589 -64.3986 18.4211 33.1589 -65.3469 -18.4211 33.1589 -65.3469 -20.7105 33.1589 -64.3986 -21.6589 33.1589 -62.1092 -21.6589 33.1589 62.1092 -20.7105 33.1589 64.3986 -18.4211 34.5 62.1092 18.4211 34.5 62.1092 18.4211 34.5 -62.1092 -18.4211 34.5 -62.1092 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | -0.0785212 -0.921027 0.381502 0.0785212 -0.921027 0.381502 0 -0.382683 0.92388 0 0 1 0 0.382683 0.92388 -0.0785212 0.921027 0.381502 0.0785212 0.921027 0.381502 0.156558 -0.912487 0.377965 0.357406 -0.357406 0.862857 0.382683 0 0.92388 0.357406 0.357406 0.862857 0.156558 0.912487 0.377965 0.381502 -0.921027 0.0785213 0.377965 -0.912487 0.156558 0.862856 -0.357407 0.357407 0.923879 0 0.382684 0.862856 0.357406 0.357407 0.377965 0.912487 0.156558 0.381502 0.921027 0.0785213 0.381502 -0.921027 -0.0785213 0.92388 -0.382683 -0 1 0 -0 0.92388 0.382683 -0 0.381502 0.921027 -0.0785213 0.377965 -0.912487 -0.156558 0.862856 -0.357406 -0.357407 0.923879 0 -0.382684 0.862856 0.357407 -0.357407 0.377965 0.912487 -0.156558 0.0785212 -0.921027 -0.381502 0.156558 -0.912487 -0.377965 0.357406 -0.357406 -0.862857 0.382683 0 -0.92388 0.357406 0.357406 -0.862857 0.156558 0.912487 -0.377965 0.0785212 0.921027 -0.381502 -0.0785212 -0.921027 -0.381502 0 -0.382683 -0.92388 0 0 -1 0 0.382683 -0.92388 -0.0785212 0.921027 -0.381502 -0.156558 -0.912487 -0.377965 -0.357406 -0.357406 -0.862857 -0.382683 0 -0.92388 -0.357406 0.357406 -0.862857 -0.156558 0.912487 -0.377965 -0.381502 -0.921027 -0.0785213 -0.377965 -0.912487 -0.156558 -0.862856 -0.357407 -0.357407 -0.923879 0 -0.382684 -0.862856 0.357406 -0.357407 -0.377965 0.912487 -0.156558 -0.381502 0.921027 -0.0785213 -0.381502 -0.921027 0.0785213 -0.92388 -0.382683 -0 -1 0 -0 -0.92388 0.382683 -0 -0.381502 0.921027 0.0785213 -0.377965 -0.912487 0.156558 -0.862856 -0.357406 0.357407 -0.923879 0 0.382684 -0.862856 0.357407 0.357407 -0.377965 0.912487 0.156558 -0.156558 -0.912487 0.377965 -0.357406 -0.357406 0.862857 -0.382683 0 0.92388 -0.357406 0.357406 0.862857 -0.156558 0.912487 0.377965 0 1 -0 0 -1 -0 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 0 0 0 0.0484494 0.836663 0.0484494 0.836663 0 0 0.0968987 0.836663 0.0968987 0 0.903101 0.836663 0.903101 0 0.951551 0.836663 0.951551 0 1 0.836663 1 0.877497 0 0.918331 0.0484494 0.918331 0.0968987 0.918331 0.903101 0.918331 0.951551 0.877497 1 0.959166 0 1 0.0484494 1 0.0968987 1 0.903101 1 0.951551 0.959166 1 0.945267 0.0484494 0.945267 0 0.945267 0.0968987 0.945267 0.903101 0.945267 0.951551 0.945267 1 0.95895 0 0.972633 0.0484494 0.972633 0.0968987 0.972633 0.903101 0.972633 0.951551 0.95895 1 0.986317 0 0.986317 1 1 1 1 0 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 237 |

1 1 3 5 1 2 4 0 1 0 0 0 5 2 2 17 2 5 16 2 4 4 2 1 17 3 5 29 3 7 28 3 6 16 3 4 29 4 7 41 4 9 40 4 8 28 4 6 41 6 9 53 6 11 52 5 10 40 5 8 6 7 13 5 1 2 1 1 12 6 8 13 18 8 14 17 8 5 5 8 2 18 9 14 30 9 15 29 9 7 17 9 5 30 10 15 42 10 16 41 10 9 29 10 7 42 11 16 53 6 17 41 6 9 7 12 19 6 13 13 1 12 18 7 14 19 19 14 20 18 14 14 6 14 13 19 15 20 31 15 21 30 15 15 18 15 14 31 16 21 43 16 22 42 16 16 30 16 15 43 18 22 53 18 23 42 17 16 2 19 25 8 19 24 7 12 1 1 12 0 8 20 24 20 20 26 19 20 4 7 20 1 20 21 26 32 21 27 31 21 6 19 21 4 32 22 27 44 22 28 43 22 8 31 22 6 44 23 28 54 23 29 53 18 10 43 18 8 9 24 31 8 19 24 2 19 30 9 25 31 21 25 32 20 25 26 8 25 24 21 26 32 33 26 33 32 26 27 20 26 26 33 27 33 45 27 34 44 27 28 32 27 27 45 28 34 54 23 35 44 23 28 10 29 19 9 30 31 2 29 36 10 31 19 22 31 20 21 31 32 9 31 31 22 32 20 34 32 21 33 32 33 21 32 32 34 33 21 46 33 22 45 33 34 33 33 33 46 35 22 54 35 37 45 34 34 3 36 3 11 36 2 10 29 1 2 29 0 11 37 2 23 37 5 22 37 4 10 37 1 23 38 5 35 38 7 34 38 6 22 38 4 35 39 7 47 39 9 46 39 8 34 39 6 47 40 9 55 40 11 54 35 10 46 35 8 12 41 13 11 36 2 3 36 12 12 42 13 24 42 14 23 42 5 11 42 2 24 43 14 36 43 15 35 43 7 23 43 5 36 44 15 48 44 16 47 44 9 35 44 7 48 45 16 55 40 17 47 40 9 13 46 19 12 47 13 3 46 18 13 48 19 25 48 20 24 48 14 12 48 13 25 49 20 37 49 21 36 49 15 24 49 14 37 50 21 49 50 22 48 50 16 36 50 15 49 52 22 55 52 23 48 51 16 0 53 25 14 53 24 13 46 1 3 46 0 14 54 24 26 54 26 25 54 4 13 54 1 26 55 26 38 55 27 37 55 6 25 55 4 38 56 27 50 56 28 49 56 8 37 56 6 50 57 28 52 57 29 55 52 10 49 52 8 15 58 31 14 53 24 0 53 30 15 59 31 27 59 32 26 59 26 14 59 24 27 60 32 39 60 33 38 60 27 26 60 26 39 61 33 51 61 34 50 61 28 38 61 27 51 62 34 52 57 35 50 57 28 4 0 19 15 63 31 0 0 36 4 64 19 16 64 20 27 64 32 15 64 31 16 65 20 28 65 21 39 65 33 27 65 32 28 66 21 40 66 22 51 66 34 39 66 33 40 5 22 52 5 37 51 67 34 53 68 39 54 68 38 55 68 10 52 68 0 3 69 0 2 69 39 1 69 38 0 69 10

238 |
239 |
240 |
241 | 242 | 243 | 244 | -40 -80.5 40 40 -80.5 40 40 -80.5 -40 -40 -80.5 -40 -40 -77.5711 47.0711 40 -77.5711 47.0711 45 -77.5711 45 47.0711 -77.5711 40 47.0711 -77.5711 -40 45 -77.5711 -45 40 -77.5711 -47.0711 -40 -77.5711 -47.0711 -45 -77.5711 -45 -47.0711 -77.5711 -40 -47.0711 -77.5711 40 -45 -77.5711 45 -40 -70.5 50 40 -70.5 50 47.0711 -70.5 47.0711 50 -70.5 40 50 -70.5 -40 47.0711 -70.5 -47.0711 40 -70.5 -50 -40 -70.5 -50 -47.0711 -70.5 -47.0711 -50 -70.5 -40 -50 -70.5 40 -47.0711 -70.5 47.0711 -40 70.5 50 40 70.5 50 47.0711 70.5 47.0711 50 70.5 40 50 70.5 -40 47.0711 70.5 -47.0711 40 70.5 -50 -40 70.5 -50 -47.0711 70.5 -47.0711 -50 70.5 -40 -50 70.5 40 -47.0711 70.5 47.0711 -40 77.5711 47.0711 40 77.5711 47.0711 45 77.5711 45 47.0711 77.5711 40 47.0711 77.5711 -40 45 77.5711 -45 40 77.5711 -47.0711 -40 77.5711 -47.0711 -45 77.5711 -45 -47.0711 77.5711 -40 -47.0711 77.5711 40 -45 77.5711 45 -40 80.5 40 40 80.5 40 40 80.5 -40 -40 80.5 -40 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | -0.0785214 -0.921027 0.381502 0.0785214 -0.921027 0.381502 0 -0.382683 0.92388 0 0 1 0 0.382683 0.92388 -0.0785214 0.921027 0.381502 0.0785214 0.921027 0.381502 0.156558 -0.912487 0.377964 0.357407 -0.357407 0.862856 0.382683 0 0.92388 0.357407 0.357407 0.862856 0.156558 0.912487 0.377964 0.381502 -0.921027 0.0785214 0.377964 -0.912487 0.156558 0.862856 -0.357407 0.357407 0.92388 0 0.382683 0.862856 0.357407 0.357407 0.377964 0.912487 0.156558 0.381502 0.921027 0.0785214 0.381502 -0.921027 -0.0785214 0.92388 -0.382683 -0 1 0 -0 0.92388 0.382683 -0 0.381502 0.921027 -0.0785214 0.377964 -0.912487 -0.156558 0.862856 -0.357407 -0.357407 0.92388 0 -0.382683 0.862856 0.357407 -0.357407 0.377964 0.912487 -0.156558 0.0785214 -0.921027 -0.381502 0.156558 -0.912487 -0.377964 0.357407 -0.357407 -0.862856 0.382683 0 -0.92388 0.357407 0.357407 -0.862856 0.156558 0.912487 -0.377964 0.0785214 0.921027 -0.381502 -0.0785214 -0.921027 -0.381502 0 -0.382683 -0.92388 0 0 -1 0 0.382683 -0.92388 -0.0785214 0.921027 -0.381502 -0.156558 -0.912487 -0.377964 -0.357407 -0.357407 -0.862856 -0.382683 0 -0.92388 -0.357407 0.357407 -0.862856 -0.156558 0.912487 -0.377964 -0.381502 -0.921027 -0.0785214 -0.377964 -0.912487 -0.156558 -0.862856 -0.357407 -0.357407 -0.92388 0 -0.382683 -0.862856 0.357407 -0.357407 -0.377964 0.912487 -0.156558 -0.381502 0.921027 -0.0785214 -0.381502 -0.921027 0.0785214 -0.92388 -0.382683 -0 -1 0 -0 -0.92388 0.382683 -0 -0.381502 0.921027 0.0785214 -0.377964 -0.912487 0.156558 -0.862856 -0.357407 0.357407 -0.92388 0 0.382683 -0.862856 0.357407 0.357407 -0.377964 0.912487 0.156558 -0.156558 -0.912487 0.377964 -0.357407 -0.357407 0.862856 -0.382683 0 0.92388 -0.357407 0.357407 0.862856 -0.156558 0.912487 0.377964 0 1 -0 0 -1 -0 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 0 0 0 0.0455526 0.835876 0.0455526 0.835876 0 0 0.0911051 0.835876 0.0911051 0 0.908895 0.835876 0.908895 0 0.954447 0.835876 0.954447 0 1 0.835876 1 0.876907 0 0.917938 0.0455526 0.917938 0.0911051 0.917938 0.908895 0.917938 0.954447 0.876907 1 0.958969 0 1 0.0455526 1 0.0911051 1 0.908895 1 0.954447 0.958969 1 1 1 1 0 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 4 4 4 3 4 4 4 3 3 4 4 4 3 4 4 280 |

1 1 3 5 1 2 4 0 1 0 0 0 5 2 2 17 2 5 16 2 4 4 2 1 17 3 5 29 3 7 28 3 6 16 3 4 29 4 7 41 4 9 40 4 8 28 4 6 41 6 9 53 6 11 52 5 10 40 5 8 6 7 13 5 1 2 1 1 12 6 8 13 18 8 14 17 8 5 5 8 2 18 9 14 30 9 15 29 9 7 17 9 5 30 10 15 42 10 16 41 10 9 29 10 7 42 11 16 53 6 17 41 6 9 7 12 19 6 13 13 1 12 18 7 14 19 19 14 20 18 14 14 6 14 13 19 15 20 31 15 21 30 15 15 18 15 14 31 16 21 43 16 22 42 16 16 30 16 15 43 18 22 53 18 23 42 17 16 2 19 3 8 19 2 7 12 1 1 12 0 8 20 2 20 20 5 19 20 4 7 20 1 20 21 5 32 21 7 31 21 6 19 21 4 32 22 7 44 22 9 43 22 8 31 22 6 44 23 9 54 23 11 53 18 10 43 18 8 9 24 13 8 19 2 2 19 12 9 25 13 21 25 14 20 25 5 8 25 2 21 26 14 33 26 15 32 26 7 20 26 5 33 27 15 45 27 16 44 27 9 32 27 7 45 28 16 54 23 17 44 23 9 10 29 19 9 30 13 2 29 18 10 31 19 22 31 20 21 31 14 9 31 13 22 32 20 34 32 21 33 32 15 21 32 14 34 33 21 46 33 22 45 33 16 33 33 15 46 35 22 54 35 23 45 34 16 3 36 3 11 36 2 10 29 1 2 29 0 11 37 2 23 37 5 22 37 4 10 37 1 23 38 5 35 38 7 34 38 6 22 38 4 35 39 7 47 39 9 46 39 8 34 39 6 47 40 9 55 40 11 54 35 10 46 35 8 12 41 13 11 36 2 3 36 12 12 42 13 24 42 14 23 42 5 11 42 2 24 43 14 36 43 15 35 43 7 23 43 5 36 44 15 48 44 16 47 44 9 35 44 7 48 45 16 55 40 17 47 40 9 13 46 19 12 47 13 3 46 18 13 48 19 25 48 20 24 48 14 12 48 13 25 49 20 37 49 21 36 49 15 24 49 14 37 50 21 49 50 22 48 50 16 36 50 15 49 52 22 55 52 23 48 51 16 0 53 3 14 53 2 13 46 1 3 46 0 14 54 2 26 54 5 25 54 4 13 54 1 26 55 5 38 55 7 37 55 6 25 55 4 38 56 7 50 56 9 49 56 8 37 56 6 50 57 9 52 57 11 55 52 10 49 52 8 15 58 13 14 53 2 0 53 12 15 59 13 27 59 14 26 59 5 14 59 2 27 60 14 39 60 15 38 60 7 26 60 5 39 61 15 51 61 16 50 61 9 38 61 7 51 62 16 52 57 17 50 57 9 4 0 19 15 63 13 0 0 18 4 64 19 16 64 20 27 64 14 15 64 13 16 65 20 28 65 21 39 65 15 27 65 14 28 66 21 40 66 22 51 66 16 39 66 15 40 5 22 52 5 23 51 67 16 53 68 25 54 68 24 55 68 10 52 68 0 3 69 0 2 69 25 1 69 24 0 69 10

281 |
282 |
283 |
284 | 285 | 286 | 287 | 0 -50 0 0 50 0 10 -50 -0 10 -50 -0 10 50 -0 10 50 -0 7.07107 -50 -7.07107 7.07107 -50 -7.07107 7.07107 50 -7.07107 7.07107 50 -7.07107 6.12323e-16 -50 -10 6.12323e-16 -50 -10 6.12323e-16 50 -10 6.12323e-16 50 -10 -7.07107 -50 -7.07107 -7.07107 -50 -7.07107 -7.07107 50 -7.07107 -7.07107 50 -7.07107 -10 -50 -1.22465e-15 -10 -50 -1.22465e-15 -10 50 -1.22465e-15 -10 50 -1.22465e-15 -7.07107 -50 7.07107 -7.07107 -50 7.07107 -7.07107 50 7.07107 -7.07107 50 7.07107 -1.83697e-15 -50 10 -1.83697e-15 -50 10 -1.83697e-15 50 10 -1.83697e-15 50 10 7.07107 -50 7.07107 7.07107 -50 7.07107 7.07107 50 7.07107 7.07107 50 7.07107 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 0 -1 -0 1 0 -0 0.707107 0 -0.707107 0 1 -0 0 0 -1 -0.707107 0 -0.707107 -1 0 -0 -0.707107 0 0.707107 0 0 1 0.707107 0 0.707107 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 0.5 0.5 0 0.5 0.146447 0.853553 0 0 0 1 0.125 1 0.125 0 1 0.5 0.853553 0.853553 0.5 1 0.25 1 0.25 0 0.375 1 0.375 0 0.5 0 0.853553 0.146447 0.625 1 0.625 0 0.146447 0.146447 0.75 1 0.75 0 0.875 1 0.875 0 1 1 1 0 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 3 4 3 3 4 3 3 4 3 3 4 3 3 4 3 3 4 3 3 4 3 3 4 3 323 |

6 0 2 2 0 1 0 0 0 7 2 6 8 2 5 4 1 4 3 1 3 9 3 8 1 3 0 5 3 7 10 0 9 6 0 2 0 0 0 11 4 11 12 4 10 8 2 5 7 2 6 13 3 9 1 3 0 9 3 8 14 0 8 10 0 9 0 0 0 15 5 13 16 5 12 12 4 10 11 4 11 17 3 2 1 3 0 13 3 9 18 0 7 14 0 8 0 0 0 19 6 14 20 6 9 16 5 12 15 5 13 21 3 1 1 3 0 17 3 2 22 0 15 18 0 7 0 0 0 23 7 17 24 7 16 20 6 9 19 6 14 25 3 18 1 3 0 21 3 1 26 0 14 22 0 15 0 0 0 27 8 20 28 8 19 24 7 16 23 7 17 29 3 14 1 3 0 25 3 18 30 0 18 26 0 14 0 0 0 31 9 22 32 9 21 28 8 19 27 8 20 33 3 15 1 3 0 29 3 14 2 0 1 30 0 18 0 0 0 3 1 24 4 1 23 32 9 21 31 9 22 5 3 7 1 3 0 33 3 15

324 |
325 |
326 |
327 | 328 | 329 | 330 | -50 0 50 50 0 50 -50 0 -50 50 0 -50 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 0 1 -0 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 0 0 0 1 1 1 1 0 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 4 366 |

1 0 3 3 0 2 2 0 1 0 0 0

367 |
368 |
369 |
370 |
371 | 372 | 373 | 374 | -23.7123 210.709 24.9106 375 | 0 1 0 -0 376 | 1 0 0 0 377 | 0 0 1 -0 378 | 1 1 1 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 3.20043 174.503 2.82738 391 | 0 1 0 -0 392 | 1 0 0 0 393 | 0 0 1 -0 394 | 1 1 1 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 0 180 -0 407 | 0 1 0 -0 408 | 1 0 0 0 409 | 0 0 1 -0 410 | 1 1 1 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 0 50 -0 423 | 0 1 0 -0 424 | 1 0 0 0 425 | 0 0 1 -0 426 | 1 1 1 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 0 0 -0 439 | 0 1 0 -0 440 | 1 0 0 0 441 | 0 0 1 -0 442 | 1 1 1 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 |
459 | --------------------------------------------------------------------------------