├── .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 | 
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 |
--------------------------------------------------------------------------------