├── .gitignore ├── README ├── index.html ├── server.js ├── style.css ├── client.js └── sb.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | socket.io 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | You need: 2 | 3 | - an iPad >= 4.2.1 4 | - node.js 5 | - socket.io (server + client) 6 | 7 | Test URL (sorry the socket server won't be online all the time): http://cubiq.org/dropbox/sb/ 8 | Lock the device rotation before trying. What you'll see is a red ball you can crontrol by tilting the iPad. 9 | If other users are connected you'll see them as grey balls. Communication happens thanks to websockets. 10 | 11 | *Not ready for production*, this is just a proof of concept. 12 | 13 | To-do: 14 | - would be nice to add collision detection between balls. 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socket Balls! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'), 2 | http = require('http'), 3 | io = require('socket.io'), 4 | server = http.createServer(), 5 | socket = io.listen(server, { transports: ['websocket']}), 6 | players = {}; 7 | 8 | // Set up events 9 | socket.on('connection', function (client) { 10 | // Send to the new user the list of active players 11 | client.send({ type: 'playerslist', list: players }); 12 | 13 | // Add the new user to the list of players 14 | players[client.sessionId] = { x:0, y:0 } 15 | 16 | // Broadcast the new user to all players 17 | socket.broadcast({ type: 'new', id: client.sessionId }, [client.sessionId]); 18 | 19 | client.on('message', function (message) { 20 | if (message.type != 'position') { 21 | return; 22 | } 23 | 24 | // Broadcast the new user position 25 | players[message.id] = { x: message.x, y: message.y }; 26 | //socket.broadcast({ type: 'position', id: message.id, x: message.x, y: message.y }, [client.sessionId]); 27 | client.send({ type: 'position', list: players }); 28 | }); 29 | 30 | client.on('disconnect', function () { 31 | // Remove the user from the list of players 32 | delete players[this.sessionId]; 33 | 34 | // Broadcast the logged out user's id 35 | socket.broadcast({ type: 'leave', id: this.sessionId }); 36 | }); 37 | 38 | }); 39 | 40 | // Start listening 41 | server.listen(8008); 42 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html, body { height:100%; } 2 | body { 3 | padding:0; margin:0; border:0; 4 | } 5 | 6 | body { 7 | background:#999; 8 | font-family:helvetica; 9 | } 10 | 11 | #stage { 12 | position:absolute; 13 | top:50%; left:50%; 14 | width:700px; height:700px; 15 | margin-top:-350px; margin-left:-350px; 16 | -webkit-border-radius:8px; 17 | background:#fff; 18 | } 19 | 20 | #popup { 21 | display:none; 22 | position:absolute; z-index:1000; 23 | width:600px; height:100px; line-height:100px; 24 | top:50%; left:50%; 25 | margin-left:-300px; margin-top:-50px; 26 | -webkit-border-radius:10px; 27 | background:rgba(0,0,0,0.5); 28 | color:#fff; 29 | text-align:center; 30 | } 31 | 32 | .ball { 33 | position:absolute; 34 | top:0; left:0; 35 | width:40px; 36 | height:40px; 37 | -webkit-border-radius:20px; 38 | -webkit-box-shadow:2px 2px 6px rgba(0,0,0,0.3); 39 | -webkit-transition-property:-webkit-transform; 40 | -webkit-transition-timing-function:linear; 41 | -webkit-transition-duration:0; 42 | -webkit-transform:translate3d(0,0,0); 43 | } 44 | 45 | .player { 46 | z-index:100; 47 | background-color:#c30; 48 | background-image:-webkit-gradient(radial, 15 15, 0, 15 15, 40, from(rgba(255,60,60,1)), color-stop(0.1, rgba(255,60,60,1)), to(rgba(20,0,0,1))); 49 | } 50 | 51 | .opponent { 52 | z-index:10; 53 | background-color:#ccc; 54 | background-image:-webkit-gradient(radial, 15 15, 0, 15 15, 40, from(rgba(250,250,250,1)), color-stop(0.1, rgba(250,250,250,1)), to(rgba(20,20,20,1))); 55 | } -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | 2 | var socket = new io.Socket(null, { port: 8008, transports: ['websocket'] }), 3 | sessionid = 0, 4 | connectTimeout; 5 | 6 | function socketInit() { 7 | var connection = socket.connect(); 8 | 9 | connectTimeout = setTimeout(function () { 10 | if (connection.connected === true) { 11 | return; 12 | } 13 | 14 | var popup = document.getElementById('popup'); 15 | popup.innerHTML = 'Sorry, the server is probably down. Retry later'; 16 | popup.style.display = 'block'; 17 | }, 3000); 18 | 19 | socket.on('connect', function () { 20 | clearTimeout(connectTimeout); 21 | 22 | sessionid = this.transport.sessionid; 23 | 24 | me = new Player(sessionid, 'player'); 25 | 26 | mainLoop = setInterval(moveMe); 27 | }); 28 | 29 | socket.on('message', function (message) { 30 | switch (message.type) { 31 | case 'position': 32 | // Update players position 33 | updatePosition(message.list); 34 | ready = true; 35 | break; 36 | case 'playerslist': 37 | // Create all opponents 38 | createOpponents(message.list); 39 | ready = true; // ready to communicate with socket server 40 | break; 41 | case 'new': 42 | // New player joined 43 | players.push(new Player(message.id, 'opponent')); 44 | break; 45 | case 'leave': 46 | // Player disconnected 47 | leave(message.id); 48 | break; 49 | } 50 | }); 51 | 52 | socket.on('disconnect', function () { 53 | document.getElementById('popup').style.display = 'block'; 54 | }); 55 | } 56 | 57 | function sendPosition () { 58 | if (ready) { 59 | ready = false; 60 | 61 | var pos = buffer.length ? buffer[0] : { x:me.x, y:me.y }; 62 | buffer.shift(); 63 | 64 | socket.send({ type:'position', id:me.id, x:pos.x, y:pos.y }); 65 | } 66 | } 67 | 68 | function updatePosition (data) { 69 | var id, i, l; 70 | 71 | for (i=0, l=players.length; i stageW - diameter) { 47 | x = stageW - diameter; 48 | that.vx *= bounce; 49 | } else if (x < 0) { 50 | x = 0; 51 | that.vx *= bounce; 52 | } 53 | 54 | if (y > stageH - diameter) { 55 | y = stageH - diameter; 56 | that.vy *= bounce; 57 | } else if (y < 0) { 58 | y = 0; 59 | that.vy *= bounce; 60 | } 61 | 62 | that.vx += ax; 63 | that.vy += ay; 64 | 65 | that.update(x, y); 66 | }, 67 | 68 | update: function (x, y) { 69 | var that = this; 70 | that.x = x; 71 | that.y = y; 72 | that.ball.style.webkitTransform = 'translate3d(' + that.x + 'px,'+ that.y +'px,0)'; 73 | }, 74 | 75 | remove: function () { 76 | stage.removeChild(this.ball); 77 | } 78 | } 79 | 80 | // Main Game Loop 81 | function moveMe () { 82 | me.move(); 83 | 84 | buffer[buffer.length] = { x: me.x, y: me.y }; 85 | 86 | if (buffer.length > 10) buffer.shift(); 87 | 88 | sendPosition(); 89 | } 90 | 91 | window.addEventListener('devicemotion', function (e) { 92 | ax = e.accelerationIncludingGravity.x * sensitivity; 93 | ay = -e.accelerationIncludingGravity.y * sensitivity; 94 | }, false); 95 | 96 | window.addEventListener('load', function () { 97 | stage = document.getElementById('stage'); 98 | stageW = stage.clientWidth; 99 | stageH = stage.clientHeight; 100 | 101 | var popup = document.getElementById('popup'); 102 | 103 | if (!navigator.appVersion.match(/ipad/gi)) { 104 | popup.innerHTML = 'Sorry, this app is for iPad only'; 105 | popup.style.display = 'block'; 106 | return; 107 | } 108 | 109 | if (!('ondevicemotion' in window)) { 110 | popup.innerHTML = 'Sorry, you need iPad ≥ 4.2 to run this app'; 111 | document.getElementById('popup').style.display = 'block'; 112 | return; 113 | } 114 | 115 | socketInit(); 116 | }, false); 117 | --------------------------------------------------------------------------------