├── .gitignore ├── src ├── assets │ ├── achievement.wav │ ├── notification.wav │ ├── controller-mode.png │ ├── couchfriends.ui.css │ └── couchfriends.ui.less ├── index.html ├── Emitter.js └── couchfriends.api.js ├── package.json ├── gruntfile.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /src/assets/achievement.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Couchfriends/Controller-API/HEAD/src/assets/achievement.wav -------------------------------------------------------------------------------- /src/assets/notification.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Couchfriends/Controller-API/HEAD/src/assets/notification.wav -------------------------------------------------------------------------------- /src/assets/controller-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Couchfriends/Controller-API/HEAD/src/assets/controller-mode.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 | Show notification hello, 1000ms
12 | show hidehowto
13 | Unlock achievement
14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "couchfriends.api", 3 | "version": "0.0.1", 4 | "description": "Enables realtime gaming with websockets", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://bitbucket.org/fellicht/couchfriends-controller-api" 11 | }, 12 | "author": "Mathieu de Ruiter", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "grunt": "^0.4.5", 16 | "grunt-contrib-copy": "^0.8.0", 17 | "grunt-contrib-jshint": "^0.11.2", 18 | "grunt-contrib-less": "^1.0.1", 19 | "grunt-contrib-uglify": "^5.0.0", 20 | "less-plugin-clean-css": "" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | uglify: { 7 | options: { 8 | sourceMap: true, 9 | sourceMapIncludeSources: true, 10 | banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n' 11 | }, 12 | build: { 13 | src: [ 14 | 'src/Emitter.js', 15 | 'src/peer.js', 16 | 'src/couchfriends.api.js' 17 | ], 18 | dest: 'build/couchfriends.api-latest.js' 19 | } 20 | }, 21 | less: { 22 | production: { 23 | options: { 24 | plugins: [ 25 | new (require('less-plugin-clean-css'))({}) 26 | ] 27 | }, 28 | files: { 29 | "build/assets/couchfriends.ui.css": "src/assets/couchfriends.ui.less" 30 | } 31 | } 32 | }, 33 | copy: { 34 | main: { 35 | src: 'src/assets/*', 36 | dest: 'build/assets/', 37 | flatten: true, 38 | expand: true, 39 | filter: 'isFile' 40 | } 41 | } 42 | }); 43 | 44 | // Load the plugin that provides the "uglify" task. 45 | grunt.loadNpmTasks('grunt-contrib-uglify'); 46 | 47 | // Default task(s). 48 | grunt.registerTask('default', ['uglify', 'less', 'copy']); 49 | 50 | grunt.loadNpmTasks('grunt-contrib-less'); 51 | 52 | grunt.loadNpmTasks('grunt-contrib-copy'); 53 | 54 | }; -------------------------------------------------------------------------------- /src/Emitter.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Expose `Emitter`. 4 | */ 5 | 6 | if (typeof module !== 'undefined') { 7 | module.exports = Emitter; 8 | } 9 | 10 | /** 11 | * Initialize a new `Emitter`. 12 | * 13 | * @api public 14 | */ 15 | 16 | function Emitter(obj) { 17 | if (obj) return mixin(obj); 18 | }; 19 | 20 | /** 21 | * Mixin the emitter properties. 22 | * 23 | * @param {Object} obj 24 | * @return {Object} 25 | * @api private 26 | */ 27 | 28 | function mixin(obj) { 29 | for (var key in Emitter.prototype) { 30 | obj[key] = Emitter.prototype[key]; 31 | } 32 | return obj; 33 | } 34 | 35 | /** 36 | * Listen on the given `event` with `fn`. 37 | * 38 | * @param {String} event 39 | * @param {Function} fn 40 | * @return {Emitter} 41 | * @api public 42 | */ 43 | 44 | Emitter.prototype.on = 45 | Emitter.prototype.addEventListener = function(event, fn){ 46 | this._callbacks = this._callbacks || {}; 47 | (this._callbacks['$' + event] = this._callbacks['$' + event] || []) 48 | .push(fn); 49 | return this; 50 | }; 51 | 52 | /** 53 | * Adds an `event` listener that will be invoked a single 54 | * time then automatically removed. 55 | * 56 | * @param {String} event 57 | * @param {Function} fn 58 | * @return {Emitter} 59 | * @api public 60 | */ 61 | 62 | Emitter.prototype.once = function(event, fn){ 63 | function on() { 64 | this.off(event, on); 65 | fn.apply(this, arguments); 66 | } 67 | 68 | on.fn = fn; 69 | this.on(event, on); 70 | return this; 71 | }; 72 | 73 | /** 74 | * Remove the given callback for `event` or all 75 | * registered callbacks. 76 | * 77 | * @param {String} event 78 | * @param {Function} fn 79 | * @return {Emitter} 80 | * @api public 81 | */ 82 | 83 | Emitter.prototype.off = 84 | Emitter.prototype.removeListener = 85 | Emitter.prototype.removeAllListeners = 86 | Emitter.prototype.removeEventListener = function(event, fn){ 87 | this._callbacks = this._callbacks || {}; 88 | 89 | // all 90 | if (0 == arguments.length) { 91 | this._callbacks = {}; 92 | return this; 93 | } 94 | 95 | // specific event 96 | var callbacks = this._callbacks['$' + event]; 97 | if (!callbacks) return this; 98 | 99 | // remove all handlers 100 | if (1 == arguments.length) { 101 | delete this._callbacks['$' + event]; 102 | return this; 103 | } 104 | 105 | // remove specific handler 106 | var cb; 107 | for (var i = 0; i < callbacks.length; i++) { 108 | cb = callbacks[i]; 109 | if (cb === fn || cb.fn === fn) { 110 | callbacks.splice(i, 1); 111 | break; 112 | } 113 | } 114 | return this; 115 | }; 116 | 117 | /** 118 | * Emit `event` with the given args. 119 | * 120 | * @param {String} event 121 | * @param {Mixed} ... 122 | * @return {Emitter} 123 | */ 124 | 125 | Emitter.prototype.emit = function(event){ 126 | this._callbacks = this._callbacks || {}; 127 | var args = [].slice.call(arguments, 1) 128 | , callbacks = this._callbacks['$' + event]; 129 | 130 | if (callbacks) { 131 | callbacks = callbacks.slice(0); 132 | for (var i = 0, len = callbacks.length; i < len; ++i) { 133 | callbacks[i].apply(this, args); 134 | } 135 | } 136 | 137 | return this; 138 | }; 139 | 140 | /** 141 | * Return array of callbacks for `event`. 142 | * 143 | * @param {String} event 144 | * @return {Array} 145 | * @api public 146 | */ 147 | 148 | Emitter.prototype.listeners = function(event){ 149 | this._callbacks = this._callbacks || {}; 150 | return this._callbacks['$' + event] || []; 151 | }; 152 | 153 | /** 154 | * Check if this emitter has `event` handlers. 155 | * 156 | * @param {String} event 157 | * @return {Boolean} 158 | * @api public 159 | */ 160 | 161 | Emitter.prototype.hasListeners = function(event){ 162 | return !! this.listeners(event).length; 163 | }; 164 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Controller API for Couchfriends 2 | With the Couchfriends Controller API you can connect your phone or tablet to your HTML5 game and use it as a controller. The Controller API uses Websockets to send and receive input. See our [Wiki](https://github.com/Couchfriends/Controller-API/wiki) for the complete documentation. 3 | 4 | ![Controller on Phone](http://couchfriends.com/img/pages/convert-smart-phone-to-gamepad-controller.png) 5 | 6 | ## Building the Couchfriends API 7 | 8 | Download or fork the source in your webroot or project directory and run: 9 | 10 | ``` 11 | npm install 12 | ``` 13 | 14 | To build the latest version run: 15 | 16 | ``` 17 | grunt 18 | ``` 19 | 20 | # Installation 21 | 22 | Add the following code in the `` of your game. 23 | ```html 24 | 25 | ``` 26 | 27 | ## Connect 28 | 29 | Couchfriends api uses the global `window.COUCHFRIENDS` or `COUCHFRIENDS` object variable. The API will automaticly connect to the websocket server. 30 | 31 | # API 32 | 33 | ### Start/host a new game 34 | 35 | You can use the `.send()` function to send data to the server or (one or all) of you connected clients. 36 | Sending data must always be an json object. This example will host a new game. See 37 | [Sending data to Players/Server](#sending-data-to-playersserver) for more examples. 38 | 39 | ## Callbacks 40 | 41 | Each data that is received from the server is passed through the `.on('type', function(){});` callback. 42 | 43 | ### on.('connect') 44 | 45 | Called after a successful connection to the Websocket server. 46 | 47 | ```javascript 48 | /** 49 | * Callback after connected to the websocket server and ready for incoming 50 | * players. 51 | * @param string code a unique identifier for players to join this game. 52 | */ 53 | COUCHFRIENDS.on('connect', function(code) { 54 | console.log('Ready for action! My gamecode is: ' + code); 55 | }); 56 | ``` 57 | 58 | ### on.('playerJoined') 59 | A new player joined the game. 60 | 61 | ```javascript 62 | /** 63 | * Callback when a player connected to the game. 64 | * 65 | * @param {object} data list with the player information 66 | * @param {int} data.id The unique identifier of the player 67 | * @param {string} [data.name] The name of the player 68 | */ 69 | COUCHFRIENDS.on('player.join', function(data) { 70 | console.log('Player joined. Player id: ' + data.id); 71 | }); 72 | ``` 73 | 74 | ```javascript 75 | /** 76 | * Player idenfifier (color). 77 | */ 78 | COUCHFRIENDS.on('player.identify', function (data) { 79 | var color = data.color; 80 | var playerId = data.player.id; 81 | // Make the player the color of the controllers layout 82 | }); 83 | ``` 84 | 85 | ### on.('playerLeft') 86 | One of the players disconnected or left the game. 87 | 88 | ```javascript 89 | /** 90 | * Callback when a player disconnect from the game. 91 | * 92 | * @param {object} data list with the player information 93 | * @param {int} data.id the unique identifier of the player that left 94 | */ 95 | COUCHFRIENDS.on('player.left', function(data) { 96 | console.log('Player left. Player id: ' + data.id); 97 | }); 98 | ``` 99 | 100 | ### on.('buttonUp') 101 | Player pressed or tapped a button. 102 | 103 | ```javascript 104 | /** 105 | * Callback when a player disconnect from the game. 106 | * 107 | * @param {object} data list with the player information 108 | * @param {int} data.id the unique identifier of the button. e.g. 'a' 109 | */ 110 | COUCHFRIENDS.on('player.buttonUp', function(data) { 111 | console.log('Player pressed button. Player id: ' + data.playerId + ' Button: ' + data.id); 112 | }); 113 | ``` 114 | 115 | ### on.('playerOrientation') 116 | A player's device orientation has changed. 117 | 118 | ```javascript 119 | /** 120 | * Callback when a player chances the orientation of his device. Useful for movement tracking. 121 | * 122 | * For performance reasons this function will only be called if the orientation has changed since the previous frame. 123 | * 124 | * @param {object} data list with the player id and orientation 125 | * @param {int} data.id The unique identifier of the player 126 | * @param {float} [data.x] The x-as orientation (-1 to 1). E.g. -0.871 127 | * @param {float} [data.y] The y-as orientation (-1 to 1). E.g. 0.12 128 | */ 129 | COUCHFRIENDS.on('player.orientation', function(data) { 130 | console.log('Player orientation changed. Player id: ' + data.player.id + ' Orientation: ' + data.x + ', ' + data.y); 131 | }); 132 | ``` 133 | 134 | ### interface.vibrate - Vibrate controller 135 | 136 | ```javascript 137 | /** 138 | * Example of letting a phone vibrate. 139 | * @param topic {string} 'interface'. 140 | * @param action {string} 'vibrate'. Bzzz 141 | * @param data {object} list with parameters. 142 | * @param data.playerId {int} The id of the player to vibrate 143 | * @param data.duration {int} The duration in ms. Maximum 1000ms. 144 | */ 145 | var jsonData = { 146 | topic: 'interface', 147 | action: 'vibrate', 148 | data: { 149 | playerId: 1234, 150 | duration: 200 151 | } 152 | }; 153 | COUCHFRIENDS.send(jsonData); 154 | ``` -------------------------------------------------------------------------------- /src/assets/couchfriends.ui.css: -------------------------------------------------------------------------------- 1 | #COUCHFRIENDS-overlay { 2 | pointer-events: none; 3 | font-family: OpenSans, "Open Sans", arial, sans-serif; 4 | color: #222222; 5 | font-size: 16px; 6 | position: fixed; 7 | z-index: 1001; 8 | left: 0; 9 | top: 0; 10 | width: 100%; 11 | height: 100%; 12 | } 13 | #COUCHFRIENDS-overlay .COUCHFRIENDS-underline { 14 | text-decoration: underline; 15 | } 16 | #COUCHFRIENDS-overlay #COUCHFRIENDS-notifications { 17 | position: absolute; 18 | width: 200px; 19 | height: auto; 20 | right: 10px; 21 | bottom: 10px; 22 | } 23 | #COUCHFRIENDS-overlay #COUCHFRIENDS-notifications .COUCHFRIENDS-notification { 24 | position: relative; 25 | overflow-y: hidden; 26 | background-color: rgba(255, 255, 255, 0.9); 27 | box-shadow: 0 0 25px rgba(0, 0, 0, 0.2); 28 | animation-name: COUCHFRIENDS-slideUp; 29 | -webkit-animation-name: COUCHFRIENDS-slideUp; 30 | margin-top: 10px; 31 | animation-duration: 0.5s; 32 | -webkit-animation-duration: 0.5s; 33 | animation-timing-function: ease; 34 | -webkit-animation-timing-function: ease; 35 | -webkit-animation-fill-mode: both; 36 | animation-fill-mode: both; 37 | } 38 | #COUCHFRIENDS-overlay #COUCHFRIENDS-notifications .COUCHFRIENDS-notification p { 39 | display: block; 40 | padding: 10px; 41 | } 42 | #COUCHFRIENDS-overlay #COUCHFRIENDS-notifications .COUCHFRIENDS-notification p:after { 43 | display: block; 44 | font-size: 0; 45 | content: " "; 46 | clear: both; 47 | height: 0; 48 | } 49 | #COUCHFRIENDS-overlay #COUCHFRIENDS-notifications .COUCHFRIENDS-notification img { 50 | float: left; 51 | margin-right: 10px; 52 | max-width: 64px; 53 | max-height: 64px; 54 | } 55 | #COUCHFRIENDS-overlay #COUCHFRIENDS-notifications .COUCHFRIENDS-notification-error { 56 | background-color: rgba(255, 0, 0, 0.5); 57 | } 58 | #COUCHFRIENDS-overlay #COUCHFRIENDS-notifications .COUCHFRIENDS-notification-achievement { 59 | background-color: rgba(88, 255, 0, 0.5); 60 | } 61 | #COUCHFRIENDS-overlay #COUCHFRIENDS-notifications .COUCHFRIENDS-notification.COUCHFRIENDS-notification-close { 62 | animation-name: COUCHFRIENDS-slideDown; 63 | -webkit-animation-name: COUCHFRIENDS-slideDown; 64 | } 65 | #COUCHFRIENDS-overlay #COUCHFRIENDS-popup { 66 | text-align: center; 67 | position: absolute; 68 | width: 25%; 69 | bottom: 0; 70 | left: 0; 71 | /* bring your own prefixes */ 72 | margin-left: -12.5%; 73 | background-color: rgba(255, 255, 255, 0.9); 74 | box-shadow: 0 0 25px rgba(0, 0, 0, 0.2); 75 | } 76 | #COUCHFRIENDS-overlay #COUCHFRIENDS-popup #COUCHFRIENDS-code { 77 | font-size: 150%; 78 | display: block; 79 | } 80 | #COUCHFRIENDS-overlay .COUCHFRIENDS-fadeIn { 81 | animation-name: COUCHFRIENDS-fadeIn; 82 | -webkit-animation-name: COUCHFRIENDS-fadeIn; 83 | animation-duration: 0.5s; 84 | -webkit-animation-duration: 0.5s; 85 | -webkit-animation-fill-mode: both; 86 | animation-fill-mode: both; 87 | } 88 | #COUCHFRIENDS-overlay .COUCHFRIENDS-fadeOut { 89 | animation-name: COUCHFRIENDS-fadeOut; 90 | -webkit-animation-name: COUCHFRIENDS-fadeOut; 91 | animation-duration: 0.5s; 92 | -webkit-animation-duration: 0.5s; 93 | -webkit-animation-fill-mode: both; 94 | animation-fill-mode: both; 95 | } 96 | #COUCHFRIENDS-overlay .COUCHFRIENDS-moveBottomLeft { 97 | animation-name: COUCHFRIENDS-moveBottomLeft; 98 | -webkit-animation-name: COUCHFRIENDS-moveBottomLeft; 99 | animation-duration: 0.5s; 100 | -webkit-animation-duration: 0.5s; 101 | -webkit-animation-fill-mode: both; 102 | animation-fill-mode: both; 103 | } 104 | #COUCHFRIENDS-overlay .COUCHFRIENDS-moveCenter { 105 | animation-name: COUCHFRIENDS-moveCenter; 106 | -webkit-animation-name: COUCHFRIENDS-moveCenter; 107 | animation-duration: 0.5s; 108 | -webkit-animation-duration: 0.5s; 109 | -webkit-animation-fill-mode: both; 110 | animation-fill-mode: both; 111 | } 112 | @keyframes COUCHFRIENDS-moveBottomLeft { 113 | 0% { 114 | font-size: 22px; 115 | line-height: 37px; 116 | padding: 15px; 117 | bottom: 50%; 118 | left: 50%; 119 | } 120 | 100% { 121 | font-size: 14px; 122 | line-height: 19px; 123 | padding: 5px; 124 | left: 13%; 125 | bottom: .5%; 126 | } 127 | } 128 | @-webkit-keyframes COUCHFRIENDS-moveBottomLeft { 129 | 0% { 130 | font-size: 22px; 131 | line-height: 37px; 132 | padding: 15px; 133 | bottom: 50%; 134 | left: 50%; 135 | } 136 | 100% { 137 | font-size: 14px; 138 | line-height: 19px; 139 | padding: 5px; 140 | left: 13%; 141 | bottom: .5%; 142 | } 143 | } 144 | @keyframes COUCHFRIENDS-moveCenter { 145 | 0% { 146 | font-size: 14px; 147 | line-height: 19px; 148 | padding: 5px; 149 | left: 13%; 150 | bottom: .5%; 151 | } 152 | 100% { 153 | font-size: 22px; 154 | line-height: 37px; 155 | padding: 15px; 156 | bottom: 50%; 157 | left: 50%; 158 | } 159 | } 160 | @-webkit-keyframes COUCHFRIENDS-moveCenter { 161 | 0% { 162 | font-size: 14px; 163 | line-height: 19px; 164 | padding: 5px; 165 | left: 13%; 166 | bottom: .5%; 167 | } 168 | 100% { 169 | font-size: 22px; 170 | line-height: 37px; 171 | padding: 15px; 172 | bottom: 50%; 173 | left: 50%; 174 | } 175 | } 176 | @keyframes COUCHFRIENDS-slideDown { 177 | 0% { 178 | max-height: 150px; 179 | } 180 | 100% { 181 | max-height: 0; 182 | } 183 | } 184 | @-webkit-keyframes COUCHFRIENDS-slideDown { 185 | 0% { 186 | max-height: 150px; 187 | } 188 | 100% { 189 | max-height: 0; 190 | } 191 | } 192 | @keyframes COUCHFRIENDS-slideUp { 193 | 0% { 194 | max-height: 0; 195 | } 196 | 100% { 197 | max-height: 150px; 198 | } 199 | } 200 | @-webkit-keyframes COUCHFRIENDS-slideUp { 201 | 0% { 202 | max-height: 0; 203 | } 204 | 100% { 205 | max-height: 150px; 206 | } 207 | } 208 | @keyframes COUCHFRIENDS-fadeIn { 209 | 0% { 210 | opacity: 0; 211 | } 212 | 100% { 213 | opacity: 1; 214 | } 215 | } 216 | @-webkit-keyframes COUCHFRIENDS-fadeIn { 217 | 0% { 218 | opacity: 0; 219 | } 220 | 100% { 221 | opacity: 1; 222 | } 223 | } 224 | @keyframes COUCHFRIENDS-fadeOut { 225 | 0% { 226 | opacity: 1; 227 | } 228 | 100% { 229 | opacity: 0; 230 | } 231 | } 232 | @-webkit-keyframes COUCHFRIENDS-fadeOut { 233 | 0% { 234 | opacity: 1; 235 | } 236 | 100% { 237 | opacity: 0; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/assets/couchfriends.ui.less: -------------------------------------------------------------------------------- 1 | @prefix: COUCHFRIENDS; 2 | @animationDuration: .5s; 3 | @boxShadow: 0 0 25px rgba(0,0,0,.2); 4 | @backgroundPopup: rgba(255,255,255,.9); 5 | @warning: rgba(255,0,0,.5); 6 | @bonus: rgba(88, 255, 0, 0.5); 7 | @fontSmall: 14px; 8 | @paddingSmall: 5px; 9 | @fontLarge: 22px; 10 | @paddingLarge: 15px; 11 | #@{prefix}-overlay { 12 | pointer-events: none; 13 | font-family: OpenSans, "Open Sans", arial, sans-serif; 14 | color: #222222; 15 | font-size: 16px; 16 | position: fixed; 17 | z-index: 1001; 18 | left: 0; 19 | top: 0; 20 | width: 100%; 21 | height: 100%; 22 | 23 | .@{prefix}-underline { 24 | text-decoration: underline; 25 | } 26 | 27 | #@{prefix}-notifications { 28 | position: absolute; 29 | width: 200px; 30 | height: auto; 31 | right: 10px; 32 | bottom: 10px; 33 | .@{prefix}-notification { 34 | position: relative; 35 | overflow-y: hidden; 36 | background-color: @backgroundPopup; 37 | box-shadow: @boxShadow; 38 | 39 | animation-name: COUCHFRIENDS-slideUp; 40 | -webkit-animation-name: COUCHFRIENDS-slideUp; 41 | margin-top: 10px; 42 | animation-duration: @animationDuration; 43 | -webkit-animation-duration: @animationDuration; 44 | animation-timing-function: ease; 45 | -webkit-animation-timing-function: ease; 46 | -webkit-animation-fill-mode: both; 47 | animation-fill-mode: both; 48 | p { 49 | display: block; 50 | padding: 10px; 51 | } 52 | p:after { 53 | display: block; 54 | font-size: 0; 55 | content: " "; 56 | clear: both; 57 | height: 0; 58 | } 59 | img { 60 | float: left; 61 | margin-right: 10px; 62 | max-width: 64px; 63 | max-height: 64px; 64 | } 65 | } 66 | .@{prefix}-notification-error { 67 | background-color: @warning; 68 | } 69 | .@{prefix}-notification-achievement { 70 | background-color: @bonus; 71 | } 72 | .@{prefix}-notification.@{prefix}-notification-close { 73 | animation-name: COUCHFRIENDS-slideDown; 74 | -webkit-animation-name: COUCHFRIENDS-slideDown; 75 | } 76 | } 77 | 78 | #@{prefix}-popup { 79 | text-align: center; 80 | position: absolute; 81 | width: 25%; 82 | bottom: 0; 83 | left: 0; 84 | /* bring your own prefixes */ 85 | //transform: translate(-50%, -50%); 86 | margin-left: -12.5%; 87 | background-color: @backgroundPopup; 88 | box-shadow: @boxShadow; 89 | #COUCHFRIENDS-code { 90 | font-size: 150%; 91 | display: block; 92 | } 93 | } 94 | .@{prefix}-fadeIn { 95 | animation-name: COUCHFRIENDS-fadeIn; 96 | -webkit-animation-name: COUCHFRIENDS-fadeIn; 97 | animation-duration: @animationDuration; 98 | -webkit-animation-duration: @animationDuration; 99 | -webkit-animation-fill-mode: both; 100 | animation-fill-mode: both; 101 | } 102 | .@{prefix}-fadeOut { 103 | animation-name: COUCHFRIENDS-fadeOut; 104 | -webkit-animation-name: COUCHFRIENDS-fadeOut; 105 | animation-duration: @animationDuration; 106 | -webkit-animation-duration: @animationDuration; 107 | -webkit-animation-fill-mode: both; 108 | animation-fill-mode: both; 109 | 110 | } 111 | .@{prefix}-moveBottomLeft { 112 | animation-name: COUCHFRIENDS-moveBottomLeft; 113 | -webkit-animation-name: COUCHFRIENDS-moveBottomLeft; 114 | animation-duration: @animationDuration; 115 | -webkit-animation-duration: @animationDuration; 116 | -webkit-animation-fill-mode: both; 117 | animation-fill-mode: both; 118 | } 119 | .@{prefix}-moveCenter { 120 | animation-name: COUCHFRIENDS-moveCenter; 121 | -webkit-animation-name: COUCHFRIENDS-moveCenter; 122 | animation-duration: @animationDuration; 123 | -webkit-animation-duration: @animationDuration; 124 | -webkit-animation-fill-mode: both; 125 | animation-fill-mode: both; 126 | 127 | } 128 | } 129 | @keyframes COUCHFRIENDS-moveBottomLeft { 130 | 0% { 131 | font-size: @fontLarge; 132 | line-height: (@fontLarge + @paddingLarge); 133 | padding: @paddingLarge; 134 | bottom: 50%; 135 | left: 50%; 136 | } 137 | 100% { 138 | font-size: @fontSmall; 139 | line-height: (@fontSmall + @paddingSmall); 140 | padding: @paddingSmall; 141 | left: 13%; 142 | bottom: .5%; 143 | } 144 | } 145 | @-webkit-keyframes COUCHFRIENDS-moveBottomLeft { 146 | 0% { 147 | font-size: @fontLarge; 148 | line-height: (@fontLarge + @paddingLarge); 149 | padding: @paddingLarge; 150 | bottom: 50%; 151 | left: 50%; 152 | } 153 | 100% { 154 | font-size: @fontSmall; 155 | line-height: (@fontSmall + @paddingSmall); 156 | padding: @paddingSmall; 157 | left: 13%; 158 | bottom: .5%; 159 | } 160 | } 161 | @keyframes COUCHFRIENDS-moveCenter { 162 | 0% { 163 | font-size: @fontSmall; 164 | line-height: (@fontSmall + @paddingSmall); 165 | padding: @paddingSmall; 166 | left: 13%; 167 | bottom: .5%; 168 | } 169 | 100% { 170 | font-size: @fontLarge; 171 | line-height: (@fontLarge + @paddingLarge); 172 | padding: @paddingLarge; 173 | bottom: 50%; 174 | left: 50%; 175 | } 176 | } 177 | @-webkit-keyframes COUCHFRIENDS-moveCenter { 178 | 0% { 179 | font-size: @fontSmall; 180 | line-height: (@fontSmall + @paddingSmall); 181 | padding: @paddingSmall; 182 | left: 13%; 183 | bottom: .5%; 184 | } 185 | 100% { 186 | font-size: @fontLarge; 187 | line-height: (@fontLarge + @paddingLarge); 188 | padding: @paddingLarge; 189 | bottom: 50%; 190 | left: 50%; 191 | } 192 | } 193 | 194 | @keyframes COUCHFRIENDS-slideDown { 195 | 0% { 196 | max-height: 150px; 197 | } 198 | 100% { 199 | max-height: 0; 200 | } 201 | } 202 | @-webkit-keyframes COUCHFRIENDS-slideDown { 203 | 0% { 204 | max-height: 150px; 205 | } 206 | 100% { 207 | max-height: 0; 208 | } 209 | } 210 | @keyframes COUCHFRIENDS-slideUp { 211 | 0% { 212 | max-height: 0; 213 | } 214 | 100% { 215 | max-height: 150px; 216 | } 217 | } 218 | 219 | @-webkit-keyframes COUCHFRIENDS-slideUp { 220 | 0% { 221 | max-height: 0; 222 | } 223 | 100% { 224 | max-height: 150px; 225 | } 226 | } 227 | 228 | @keyframes COUCHFRIENDS-fadeIn { 229 | 0% { 230 | opacity: 0; 231 | } 232 | 100% { 233 | opacity: 1; 234 | } 235 | } 236 | @-webkit-keyframes COUCHFRIENDS-fadeIn { 237 | 0% { 238 | opacity: 0; 239 | } 240 | 100% { 241 | opacity: 1; 242 | } 243 | } 244 | @keyframes COUCHFRIENDS-fadeOut { 245 | 0% { 246 | opacity: 1; 247 | } 248 | 100% { 249 | opacity: 0; 250 | } 251 | } 252 | 253 | @-webkit-keyframes COUCHFRIENDS-fadeOut { 254 | 0% { 255 | opacity: 1; 256 | } 257 | 100% { 258 | opacity: 0; 259 | } 260 | } -------------------------------------------------------------------------------- /src/couchfriends.api.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Couchfriends controller api. With the Couchfriends Controller API you can connect your phone or tablet to your HTML5 4 | * game and use it as a controller. The Controller API uses WebRTC (peer2peer) to send and receive input. 5 | * 6 | * @copyright (c) 2015 Mathieu de Ruiter, Couchfriends, Fellicht & Editors 7 | * @author Mathieu de Ruiter / http://www.fellicht.nl/ 8 | * 9 | * For detailed information about the development with the Couchfriends API please visit https://couchfriends.com. 10 | * Please do not remove the header of this file. 11 | */ 12 | 13 | var COUCHFRIENDS = { 14 | REVISION: '4', 15 | /** 16 | * Array with sounds 17 | * @author http://opengameart.org/users/draconx 18 | */ 19 | _sounds: { 20 | achievement: { 21 | play: function () { 22 | return false; 23 | }, // In case the file can't be loaded 24 | file: 'achievement.wav' 25 | }, 26 | notification: { 27 | play: function () { 28 | return false; 29 | }, // In case the file can't be loaded 30 | file: 'notification.wav' 31 | } 32 | }, 33 | /** 34 | * Url/path to assets 35 | */ 36 | _baseUrl: 'https://couchfriends.com/src/api/build/assets/', 37 | /** 38 | * All connected players with their id, connection object, name 39 | */ 40 | playerIndex: 1, 41 | players: [], 42 | socket: {}, // The Websocket object 43 | _code: '', 44 | // Object with current information and state over the game 45 | status: { 46 | connected: false 47 | }, 48 | /** 49 | * Global settings for COUCHFRIENDS api 50 | * @type {object} settings list of settings 51 | */ 52 | settings: { 53 | /** 54 | * The current color index. 55 | */ 56 | colorIndex: 0, 57 | /** 58 | * Available player colors. 59 | */ 60 | colors: [ 61 | '#ff0000', 62 | '#00ff00', 63 | '#0000ff', 64 | '#ffff00', 65 | '#ff00ff', 66 | '#00ffff', 67 | '#ff9900', 68 | '#6d00ff', 69 | '#810000', 70 | '#008100', 71 | '#000081', 72 | '#818100', 73 | '#810081', 74 | '#008181', 75 | '#814c00', 76 | '#370081', 77 | '#ff7d7d', 78 | '#7dff7d', 79 | '#7d7dff', 80 | '#ffff7d', 81 | '#ff7dff', 82 | '#7dffff', 83 | '#ffcf8b', 84 | '#a983ff' 85 | ], 86 | /** 87 | * UI Settings 88 | */ 89 | ui: { 90 | displayCode: true, // Show the code to join 91 | showNotifications: true, 92 | sound: true 93 | } 94 | } 95 | }; 96 | 97 | /** 98 | * Init some javascript and styles to the game for dynamic overviews 99 | */ 100 | COUCHFRIENDS.init = function () { 101 | var head = document.getElementsByTagName('head')[0]; 102 | var link = document.createElement('link'); 103 | link.rel = 'stylesheet'; 104 | link.type = 'text/css'; 105 | link.href = COUCHFRIENDS._baseUrl + 'couchfriends.ui.css'; 106 | link.media = 'all'; 107 | head.appendChild(link); 108 | var containerDiv = document.createElement("div"); 109 | containerDiv.id = 'COUCHFRIENDS-overlay'; 110 | containerDiv.innerHTML = '
'; 111 | document.body.appendChild(containerDiv); 112 | COUCHFRIENDS._loadAudio(); 113 | COUCHFRIENDS.connect(); 114 | }; 115 | 116 | document.addEventListener('DOMContentLoaded', COUCHFRIENDS.init, false); 117 | 118 | /** 119 | * Load all audio files 120 | * @private 121 | */ 122 | COUCHFRIENDS._loadAudio = function () { 123 | 124 | if (COUCHFRIENDS.settings.ui.sound == false) { 125 | return false; 126 | } 127 | if (typeof AudioContext != 'function') { 128 | return false; 129 | } 130 | 131 | for (var key in COUCHFRIENDS._sounds) { 132 | if (!COUCHFRIENDS._sounds.hasOwnProperty(key)) { 133 | continue; 134 | } 135 | var sound = COUCHFRIENDS._sounds[key]; 136 | var request = new XMLHttpRequest(); 137 | request.open('GET', COUCHFRIENDS._baseUrl + sound.file, true); 138 | request.responseType = 'arraybuffer'; 139 | request.key = key; 140 | request.onload = function () { 141 | var context = new AudioContext(); 142 | context.key = this.key; 143 | context.decodeAudioData(this.response, function (buffer) { 144 | COUCHFRIENDS._sounds[context.key].play = function () { 145 | var source = context.createBufferSource(); 146 | source.buffer = buffer; 147 | source.connect(context.destination); 148 | if (!source.start) 149 | source.start = source.noteOn; 150 | source.start(0); 151 | } 152 | }); 153 | }; 154 | request.send(); 155 | } 156 | }; 157 | 158 | /** 159 | * Show notification and remove it after a short delay 160 | * @param message 161 | * @param duration the duration in ms 162 | * @param options object List with options 163 | * @param options.type string Type of the notification. Options: 'default', 'error', 'achievement' 164 | * @param options.sound boolean Play the default notification sound. 165 | */ 166 | COUCHFRIENDS.showNotification = function (message, duration, options) { 167 | options = options || {}; 168 | if (COUCHFRIENDS.settings.ui.showNotifications == false) { 169 | return; 170 | } 171 | var defaultOptions = { 172 | type: 'default', 173 | sound: true 174 | }; 175 | options = Object.assign(defaultOptions, options); 176 | duration = duration || 3500; 177 | if (COUCHFRIENDS.settings.ui.sound && options.sound) { 178 | COUCHFRIENDS._sounds.notification.play(); 179 | } 180 | var id = Date.now(); 181 | var notificationEl = document.createElement("div"); 182 | notificationEl.className = 'COUCHFRIENDS-notification COUCHFRIENDS-notification-' + options.type; 183 | notificationEl.id = 'COUCHFRIENDS-' + id; 184 | notificationEl.innerHTML = '

' + message + '

'; 185 | document.getElementById('COUCHFRIENDS-notifications').appendChild(notificationEl); 186 | setTimeout(function () { 187 | document.getElementById('COUCHFRIENDS-' + id).className = 'COUCHFRIENDS-notification COUCHFRIENDS-notification-' + options.type + ' COUCHFRIENDS-notification-close'; 188 | setTimeout(function () { 189 | var node = document.getElementById('COUCHFRIENDS-' + id); 190 | if (node.parentNode) { 191 | node.parentNode.removeChild(node); 192 | } 193 | }, 1000); 194 | }, duration); 195 | }; 196 | 197 | COUCHFRIENDS.showHideHowToPopup = function () { 198 | if (COUCHFRIENDS.settings.displayCode == false) { 199 | document.getElementById('COUCHFRIENDS-popup').style.display = 'none'; 200 | return; 201 | } 202 | if (COUCHFRIENDS.players.length > 0 || COUCHFRIENDS._code == '') { 203 | if (document.getElementById('COUCHFRIENDS-popup').offsetParent === null) { 204 | return; 205 | } 206 | document.getElementById('COUCHFRIENDS-popup').className = 'COUCHFRIENDS-moveBottomLeft'; 207 | return; 208 | } 209 | var message = ' Go to couchfriends.com with your phone or tablet and enter the code ' + COUCHFRIENDS._code + ''; 210 | document.getElementById('COUCHFRIENDS-popup').innerHTML = message; 211 | if (document.getElementById('COUCHFRIENDS-popup').offsetParent !== null) { 212 | document.getElementById('COUCHFRIENDS-popup').className = 'COUCHFRIENDS-moveCenter'; 213 | } 214 | }; 215 | 216 | /** 217 | * Generate a "random" color for the player. This is handy for creating 218 | * unique player indications. The color is sent back to the controller. 219 | * @returns {string} 220 | * @private 221 | */ 222 | COUCHFRIENDS._generateColor = function () { 223 | var color = "#" + ((1 << 24) * Math.random() | 0).toString(16); 224 | var colorIndex = COUCHFRIENDS.settings.colorIndex; 225 | if (COUCHFRIENDS.settings.colors[colorIndex] != null) { 226 | color = COUCHFRIENDS.settings.colors[colorIndex]; 227 | colorIndex++; 228 | } 229 | else { 230 | colorIndex = 0; 231 | color = COUCHFRIENDS.settings.colors[colorIndex]; 232 | colorIndex++; 233 | } 234 | COUCHFRIENDS.settings.colorIndex = colorIndex; 235 | return color; 236 | }; 237 | 238 | /** 239 | * Generate a code. Code should always be in capitals. Controller will uppercase all chars. 240 | * @param len 241 | * @returns {string} 242 | * @private 243 | */ 244 | COUCHFRIENDS._generateCode = function (len) { 245 | len = len || 3; 246 | var text = ""; 247 | var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // Caps only 248 | for (var i = 0; i < len; i++) 249 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 250 | 251 | return text; 252 | }; 253 | 254 | /** 255 | * Connect function. This will connect the game to the websocket server. 256 | * 257 | * @returns {void|boolean} false on error or return void. See the .on('connect', function() { }) callback for more info. 258 | */ 259 | COUCHFRIENDS.connect = function () { 260 | 261 | var id = 'lwjd5qra8257b9'; 262 | var code = COUCHFRIENDS._generateCode(3); 263 | var peer = new Peer(code, { 264 | // key: id 265 | }); 266 | peer.on('open', function (code) { 267 | COUCHFRIENDS.emit('connect', code); 268 | }); 269 | peer.on('close', function () { 270 | COUCHFRIENDS.emit('disconnect'); 271 | }); 272 | peer.on('error', function (error) { 273 | console.log(error); 274 | }); 275 | peer.on('connection', function (conn) { 276 | COUCHFRIENDS.emit('player.connected', conn); 277 | }); 278 | COUCHFRIENDS._socket = peer; 279 | }; 280 | 281 | /** 282 | * Send data to the server/controller 283 | * 284 | * @param data Object object with data to send. See Api references for all available options. 285 | */ 286 | COUCHFRIENDS.send = function (data) { 287 | if (data.id == null) { 288 | console.warn(data); 289 | return; 290 | } 291 | for (var i = 0; i < this.players.length; i++) { 292 | if (data.id == this.players[i].id) { 293 | this.players[i].conn.send(data); 294 | break; 295 | } 296 | } 297 | }; 298 | 299 | Emitter(COUCHFRIENDS); 300 | 301 | /** 302 | * Callback when an error has occurred. 303 | * 304 | * @param {object} data list with error details. 305 | * @param {string} data.message the error 306 | */ 307 | COUCHFRIENDS.on('error', function (data) { 308 | COUCHFRIENDS.showNotification(data, null, { 309 | type: 'error' 310 | }) 311 | }); 312 | 313 | /** 314 | * Callback after connection to the WebSocket server is successful. Best practise will be hosting a new game after 315 | * a successful connection. 316 | * @param string code. The code players can use to join. 317 | */ 318 | COUCHFRIENDS.on('connect', function (key) { 319 | COUCHFRIENDS._code = key; 320 | COUCHFRIENDS.showHideHowToPopup(); 321 | }); 322 | 323 | /** 324 | * Callback after the connection is lost from the WebSocket server. 325 | */ 326 | COUCHFRIENDS.on('disconnect', function () { 327 | COUCHFRIENDS.showNotification('Disconnected from server...', null, { 328 | type: 'error' 329 | }); 330 | }); 331 | 332 | /** 333 | * Callback when a player connected to the game. 334 | * 335 | * @param conn the peer connection to the player. 336 | */ 337 | COUCHFRIENDS.on('player.connected', function (conn) { 338 | COUCHFRIENDS.playerIndex++; 339 | var playerId = COUCHFRIENDS.playerIndex; 340 | var player = { 341 | id: playerId, 342 | peer: playerId, // fallback 343 | conn: conn, 344 | color: COUCHFRIENDS._generateColor() 345 | }; 346 | conn.player = player; 347 | conn.on('open', function () { 348 | 349 | /** 350 | * Receiving data from one of the players. 351 | * @param data object from the controller 352 | * @param data.topic string The action of the player 353 | * player.orientation 354 | * player.click 355 | * player.clickDown 356 | * player.clickUp 357 | * player.buttonClick 358 | * player.buttonDown 359 | * player.buttonUp 360 | * player.identify 361 | * 362 | * @return void 363 | */ 364 | conn.on('data', function (data) { 365 | var action = ''; 366 | if (data.topic === null) { 367 | return; 368 | } 369 | action = data.topic; 370 | if (data.action) { 371 | action += '.' + data.action; 372 | } 373 | var params = {}; 374 | if (data.data != null) { 375 | params = data.data; 376 | } 377 | params.player = this.player; 378 | COUCHFRIENDS.emit(action, params); 379 | }); 380 | 381 | COUCHFRIENDS.emit('player.join', this.player); 382 | conn.send({ 383 | type: 'player.identify', 384 | data: { 385 | color: this.player.color 386 | } 387 | }); 388 | conn.send({ 389 | type: 'game.start' 390 | }); 391 | COUCHFRIENDS.emit('player.identify', { 392 | color: this.player.color, 393 | player: { 394 | id: this.player.id 395 | }}); 396 | 397 | }); 398 | conn.on('close', function () { 399 | COUCHFRIENDS.emit('player.left', { 400 | player: this.player 401 | }); 402 | }); 403 | COUCHFRIENDS.players.push(player); 404 | COUCHFRIENDS.showNotification('New player joined.'); 405 | COUCHFRIENDS.showHideHowToPopup(); 406 | }); 407 | 408 | /** 409 | * Callback when a player disconnect from the game. 410 | * 411 | * @param {object} data list with the player information 412 | * @param {int} data.player the player object 413 | */ 414 | COUCHFRIENDS.on('player.left', function (data) { 415 | COUCHFRIENDS.players.splice(COUCHFRIENDS.players.indexOf(data.player), 1); 416 | COUCHFRIENDS.showNotification('Player left.'); 417 | COUCHFRIENDS.showHideHowToPopup(); 418 | }); 419 | 420 | /** 421 | * Callback when achievement is unlocked. Displays notification and plays 422 | * a achievement sound. 423 | * @param object data 424 | * data.name the name of the achievement 425 | * data.image the url of the icon of the achievement 426 | */ 427 | COUCHFRIENDS.on('achievementUnlock', function (data) { 428 | COUCHFRIENDS._sounds.achievement.play(); 429 | var html = ''; 430 | if (data.image != null) { 431 | html += ' '; 432 | } 433 | html += 'Achievement unlocked: ' + data.name + ''; 434 | COUCHFRIENDS.showNotification(html, null, { 435 | type: 'achievement', 436 | sound: false 437 | }); 438 | }); 439 | 440 | /** 441 | * Callback when a player chances the orientation of his device. Useful for movement tracking. 442 | * 443 | * For performance reasons this function will only be called if the orientation has changed since the previous frame. 444 | * 445 | * @param {object} data list with the player id and orientation 446 | * @param {int} data.player The player object 447 | * @param {float} [data.orientation.x] The x-as orientation (-1 to 1). E.g. -0.871 448 | * @param {float} [data.orientation.y] The y-as orientation (-1 to 1). E.g. 0.12 449 | * @param {float} [data.orientation.z] The z-as orientation (-1 to 1). E.g. -0.301 450 | */ 451 | // COUCHFRIENDS.on('player.orientation', function (data) { 452 | // console.log('Player orientation changed. Player id: ' + data.id + ' Orientation: ' + data.orientation.x + ', ' + data.orientation.y + ', ' + data.orientation.z); 453 | // }); 454 | 455 | /** 456 | * Callback when a player changed its name or added additional information like selected color. 457 | * 458 | * @param {object} data list with the player information 459 | * @param {int} data.id The unique identifier of the player 460 | * @param {float} [data.name] The (new) name of the player. See http://couchfriends.com/pages/profile.html for possible 461 | * names and characters that might be included in the name. 462 | */ 463 | COUCHFRIENDS.on('player.identify', function (data) { 464 | //console.log('Player with id: '+ data.id +' changed its name to: ' + data.name); 465 | }); 466 | 467 | /** 468 | * Callback when a player tapped canvas up and down 469 | * 470 | * @param {object} data list with the player information 471 | * @param {int} data.id The unique identifier of the player 472 | * @param {float} data.x Left position clicked in percentage 473 | * @param {float} data.y Top position clicked in percentage 474 | */ 475 | COUCHFRIENDS.on('player.click', function (data) { 476 | //console.log('Player clicked. Player id: ' + data.id + ' Click position: ' + data.x + ', ' + data.y); 477 | }); 478 | 479 | /** 480 | * Callback when a player tapped canvas up and down 481 | * 482 | * @param {object} data list with the player information 483 | * @param {int} data.id The unique identifier of the player 484 | * @param {float} data.x Left position clicked in percentage 485 | * @param {float} data.y Top position clicked in percentage 486 | */ 487 | COUCHFRIENDS.on('player.clickDown', function (data) { 488 | //console.log('Player clicked. Player id: ' + data.id + ' Click position: ' + data.x + ', ' + data.y); 489 | }); 490 | 491 | /** 492 | * Callback when a player tapped canvas up and down 493 | * 494 | * @param {object} data list with the player information 495 | * @param {int} data.id The unique identifier of the player 496 | * @param {float} data.x Left position clicked in percentage 497 | * @param {float} data.y Top position clicked in percentage 498 | */ 499 | COUCHFRIENDS.on('player.clickUp', function (data) { 500 | //console.log('Player clicked. Player id: ' + data.id + ' Click position: ' + data.x + ', ' + data.y); 501 | }); 502 | 503 | /** 504 | * Callback when a player tapped a button 505 | * 506 | * @param {object} data list with the player and button information 507 | * @param {int} data.id The unique identifier of the button 508 | * @param {int} data.playerId The unique identifier of the player 509 | */ 510 | COUCHFRIENDS.on('player.buttonClick', function (data) { 511 | //console.log('Player clicked a button. Player id: ' + data.playerId + ' Button id: ' + data.id); 512 | }); 513 | 514 | /** 515 | * Callback when a player tapped a button 516 | * 517 | * @param {object} data list with the player and button information 518 | * @param {int} data.player the player object 519 | * @param {object} data.button Object of the button information 520 | * @param {string} data.button.id The Button id 521 | */ 522 | COUCHFRIENDS.on('player.buttonDown', function (data) { 523 | //console.log('Player clicked a button. Player id: ' + data.playerId + ' Button id: ' + data.button.id); 524 | }); 525 | 526 | /** 527 | * Callback when a player released a button 528 | * 529 | * @param {object} data list with the player and button information 530 | * @param {int} data.player the player object 531 | * @param {object} data.button Object of the button information 532 | * @param {string} data.button.id The Button id 533 | */ 534 | COUCHFRIENDS.on('player.buttonUp', function (data) { 535 | //console.log('Player clicked a button. Player id: ' + data.playerId + ' Button id: ' + data.button.id); 536 | }); 537 | --------------------------------------------------------------------------------