├── README.md ├── .gitignore ├── public ├── images │ ├── bg.png │ └── indutny.png ├── stylesheets │ └── style.css └── javascripts │ └── main.js ├── routes └── index.js ├── package.json ├── autorestart.js ├── app.js ├── views └── index.jade └── home └── realtime.js /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /public/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indutny/home/HEAD/public/images/bg.png -------------------------------------------------------------------------------- /public/images/indutny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indutny/home/HEAD/public/images/indutny.png -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var package = require('../package'); 2 | 3 | /* 4 | * GET home page. 5 | */ 6 | 7 | exports.index = function(req, res){ 8 | res.render('index', { 9 | title: 'Fedor Indutny', 10 | layout: null, 11 | version: package.version 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "home", 3 | "version": "0.1.5", 4 | "private": true, 5 | "dependencies": { 6 | "express": "2.5.x", 7 | "jade": ">= 0.0.1", 8 | "socket.io": "0.8.x", 9 | "redis": "0.7.x", 10 | "hiredis": "0.1.x", 11 | "sticky-session": "0.0.x" 12 | }, 13 | "databases": { 14 | "main": "redis" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /autorestart.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | spawn = require('child_process').spawn; 3 | 4 | function start() { 5 | var child = spawn(process.execPath, [path.resolve(__dirname, 'app.js')]); 6 | process.stderr.write('started child with pid: ' + child.pid + '\n'); 7 | 8 | child.on('exit', function(code) { 9 | process.stderr.write('child exit: ' + code + '!\n'); 10 | setTimeout(start, 100); 11 | }); 12 | child.stdout.pipe(process.stdout); 13 | child.stderr.pipe(process.stderr); 14 | } 15 | start(); 16 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var package = require('./package'), 7 | sticky = require('sticky-session'), 8 | os = require('os'), 9 | express = require('express'), 10 | io = require('socket.io'), 11 | routes = require('./routes'); 12 | 13 | var app = module.exports = express.createServer(); 14 | 15 | // Configuration 16 | 17 | app.configure(function(){ 18 | app.set('views', __dirname + '/views'); 19 | app.set('view engine', 'jade'); 20 | app.use(express.bodyParser()); 21 | app.use(express.methodOverride()); 22 | app.use(app.router); 23 | app.use(express.static(__dirname + '/public')); 24 | }); 25 | 26 | app.configure('development', function(){ 27 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 28 | }); 29 | 30 | app.configure('production', function(){ 31 | app.use(express.errorHandler()); 32 | }); 33 | 34 | // Routes 35 | 36 | app.get('/', routes.index); 37 | 38 | sticky(function() { 39 | io = io.listen(app); 40 | io.disable('log'); 41 | 42 | // App-Specific stuff 43 | require('./home/realtime').init(io, { 44 | version: package.version, 45 | redis: { 46 | port: +process.env['DB-MAIN-PORT'] || 6379, 47 | host: +process.env['DB-MAIN-HOST'] || 'localhost', 48 | password: process.env['DB-MAIN-PASSWORD'], 49 | channel: 'guys' 50 | } 51 | }); 52 | 53 | return app; 54 | }).listen(3000, function() { 55 | if (this.address) { 56 | console.log('Started listening on port: %d', this.address().port); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | meta(property='og:title', content='Fedor Indutny - Home page') 5 | meta(property='og:type', content='website') 6 | meta(property='og:url', content='http://indutny.com/') 7 | meta(property='og:image', content='') 8 | meta(property='og:site_name', content='Fedor Indutny') 9 | meta(property='fb:app_id', content='328736900495928') 10 | title= title 11 | link(rel='stylesheet', href='/stylesheets/style.css?v=' + version) 12 | body 13 | section#content-wrap 14 | section#content 15 | canvas#drawer(width=560,height=346) 16 | section#contacts 17 | div.contact.github 18 | a(target='_blank', href='https://github.com/indutny') github 19 | div.contact.geeklist 20 | a(target='_blank', href='http://geekli.st/indutny') geeklist 21 | div.contact.twitter 22 | a(target='_blank', href='http://twitter.com/indutny') twitter 23 | img(src='/images/indutny.png') 24 | footer 25 | div.twitter 26 | a(href='https://twitter.com/share',class='twitter-share-button',data-via='indutny') Tweet 27 | script !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src='//platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document,'script','twitter-wjs'); 28 | div.facebook 29 | div#fb-root 30 | script (function(d, s, id) {var js, fjs = d.getElementsByTagName(s)[0];if (d.getElementById(id)) return;js = d.createElement(s); js.id = id; js.src = '//connect.facebook.net/en_US/all.js#xfbml=1'; fjs.parentNode.insertBefore(js, fjs); }(document, 'script', 'facebook-jssdk')); 31 | div.fb-like(data-send='false',data-layout='button_count',data-width='100',data-show-faces='false') 32 | div.gplus 33 | div(class='g-plusone',data-size='small',data-annotation='inline') 34 | script (function() {var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;po.src = 'https://apis.google.com/js/plusone.js';var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s); })(); 35 | script(src='/socket.io/socket.io.js') 36 | script(src='/javascripts/main.js?v=' + version) 37 | script var _gaq =_gaq||[];_gaq.push(['_setAccount', 'UA-29169083-1']);_gaq.push(['_trackPageview']);(function() {var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);})(); 38 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 14px Helvetica; 3 | margin: 0; 4 | padding: 0; 5 | background: #3B5078 url(/images/bg.png) 50% 50% repeat; 6 | } 7 | 8 | a { 9 | color: #2B304A; 10 | text-decoration: none; 11 | } 12 | 13 | a:hover { 14 | color: #3B5078; 15 | text-decoration: underline; 16 | } 17 | 18 | footer { 19 | position: fixed; 20 | bottom: 0; 21 | width: 100%; 22 | padding-left: 8px; 23 | line-height: 24px; 24 | height: 24px; 25 | background: rgba(255,255,255,0.6); 26 | border-top: 1px solid rgba(255,255,255,0.7); 27 | } 28 | 29 | footer div { 30 | display: inline-block; 31 | margin-right: 4px; 32 | } 33 | 34 | footer .twitter { 35 | margin-top: 2px; 36 | } 37 | 38 | footer .facebook { 39 | position: relative; 40 | top: -3px; 41 | } 42 | 43 | footer .gplus { 44 | position: relative; 45 | top: -2px; 46 | } 47 | 48 | section#content-wrap { 49 | position: absolute; 50 | width: 560px; 51 | height: 346px; 52 | margin-top: -178px; 53 | margin-left: -280px; 54 | top: 50%; 55 | left: 50%; 56 | } 57 | 58 | section#content { 59 | position: relative; 60 | box-sizing: border-box; 61 | -webkit-box-sizing: border-box; 62 | -moz-box-sizing: border-box; 63 | -o-box-sizing: border-box; 64 | 65 | width: 560px; 66 | height: 346px; 67 | 68 | padding: 8px; 69 | background: #fff; 70 | 71 | text-align:center; 72 | 73 | border-radius: 4px; 74 | box-shadow: 0 0 14px rgba(255,255,255,0.4); 75 | } 76 | 77 | section#content:before, section#content:after { 78 | position: absolute; 79 | bottom: 0; 80 | content: ''; 81 | z-index: -1; 82 | background: rgba(0,0,0,0.3); 83 | 84 | width: 70%; 85 | height: 30%; 86 | 87 | border-radius: 4px; 88 | box-shadow: 0 -8px 8px rgba(0,0,0,0.1); 89 | -webkit-transform: skew(15deg); 90 | -moz-transform: skew(15deg); 91 | -o-transform: skew(15deg); 92 | transform: skew(15deg); 93 | } 94 | 95 | section#content:before { 96 | left: -14px; 97 | } 98 | 99 | section#content:after { 100 | right: -14px; 101 | -webkit-transform: skew(-15deg); 102 | -moz-transform: skew(-15deg); 103 | -o-transform: skew(-15deg); 104 | transform: skew(-15deg); 105 | } 106 | 107 | section#content img { 108 | display: block; 109 | position: absolute; 110 | width: 409px; 111 | height: 115px; 112 | top: 115px; 113 | left: 75px; 114 | 115 | -webkit-user-select: none; 116 | -moz-user-select: none; 117 | -o-user-select: none; 118 | user-select: none; 119 | -webkit-user-drag: none; 120 | -moz-user-drag: none; 121 | -o-user-drag: none; 122 | user-drag:none; 123 | } 124 | 125 | section#contacts { 126 | display: none; 127 | opacity: 0; 128 | 129 | z-index: 2; 130 | position: absolute; 131 | left: 0; 132 | bottom: 46px; 133 | width: 100%; 134 | text-align: center; 135 | } 136 | 137 | section#contacts.visible { 138 | display: block; 139 | transition: opacity ease-in 0.5s; 140 | -webkit-transition: opacity ease-in 0.5s; 141 | -moz-transition: opacity ease-in 0.5s; 142 | } 143 | 144 | section#contacts.visible.full { 145 | opacity: 1; 146 | } 147 | 148 | 149 | section#contacts .contact { 150 | display: inline-block; 151 | margin: 0 32px; 152 | padding: 0 6px 0 24px; 153 | height: 24px; 154 | line-height: 24px; 155 | background: transparent url() no-repeat 3px 50%; 156 | 157 | border: 1px solid rgba(0, 0, 0, 0.2); 158 | border-radius: 3px; 159 | -webkit-border-radius: 3px; 160 | -moz-border-radius: 3px; 161 | -o-border-radius: 3px; 162 | 163 | box-shadow: inset 0 0 7px rgba(0, 0, 0, 0.05); 164 | } 165 | 166 | section#contacts .contact.github { 167 | background-image: url(https://github.com/favicon.ico); 168 | } 169 | 170 | section#contacts .contact.geeklist { 171 | background-image: url(http://geekli.st/favicon.ico); 172 | } 173 | 174 | section#contacts .contact.twitter { 175 | background-image: url(https://twitter.com/phoenix/favicon.ico); 176 | } 177 | 178 | section#content canvas { 179 | top: 0; 180 | left: 0; 181 | z-index: 2; 182 | position: absolute; 183 | } 184 | -------------------------------------------------------------------------------- /home/realtime.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | redis = require('redis'); 3 | 4 | function Guy(pool, pid, id, ip) { 5 | this.pool = pool; 6 | this.pid = pid; 7 | this.id = id; 8 | this.ip = ip; 9 | this.mode = 'standby'; 10 | this.text = ''; 11 | this.pos = { x: 0, y: 0 }; 12 | this.timeout = undefined; 13 | this.destroyed = false; 14 | 15 | this.ping(); 16 | }; 17 | 18 | Guy.prototype.ping = function ping() { 19 | if (this.destroyed) return; 20 | 21 | var self = this; 22 | clearTimeout(this.timeout); 23 | this.timeout = setTimeout(function() { 24 | self.destroyed = true; 25 | self.pool.remove(self); 26 | }, 30000); 27 | }; 28 | 29 | function PMap(pool, pid) { 30 | this.pool = pool; 31 | this.pid = pid; 32 | this.guys = {}; 33 | 34 | this.destroyed = false; 35 | this.timer = undefined; 36 | this.ping(); 37 | }; 38 | 39 | PMap.prototype.ping = function ping() { 40 | if (this.destroyed) return; 41 | 42 | var self = this; 43 | clearTimeout(this.timer); 44 | this.timer = setTimeout(function() { 45 | self.remove(); 46 | }, 15000); 47 | }; 48 | 49 | PMap.prototype.remove = function remove() { 50 | this.destroyed = true; 51 | 52 | delete this.pool.pmap[this.pid]; 53 | 54 | var self = this, 55 | guys = this.guys; 56 | 57 | Object.keys(guys).forEach(function(key) { 58 | self.pool.remove(guys[key]); 59 | }); 60 | }; 61 | 62 | function GuysPool(io, options) { 63 | process.EventEmitter.call(this); 64 | 65 | this.version = options.version; 66 | this.id = (~~(Math.random() * 1e9)).toString(36); 67 | this.io = io; 68 | this.options = options; 69 | this.pool = []; 70 | this.map = {}; 71 | this.pmap = {}; 72 | 73 | this.publish = this._createRedis(); 74 | this.subscribe = this._createRedis(); 75 | this.subscribe.subscribe(options.redis.channel); 76 | 77 | this.subscribe.on('message', this.onMessage.bind(this)); 78 | this.manageIo(io); 79 | 80 | var self = this; 81 | 82 | this.keepAliveTimer = setInterval(function() { 83 | self.publish.publish( 84 | self.options.redis.channel, 85 | JSON.stringify(['ping', self.id]) 86 | ); 87 | }, 5000); 88 | 89 | this.bootstrap(); 90 | }; 91 | util.inherits(GuysPool, process.EventEmitter); 92 | 93 | GuysPool.prototype.bootstrap = function bootstrap() { 94 | this.publish.publish( 95 | this.options.redis.channel, 96 | JSON.stringify(['bootstrap', this.id]) 97 | ); 98 | }; 99 | 100 | GuysPool.prototype._createRedis = function _createRedis() { 101 | var client = redis.createClient( 102 | this.options.redis.port, 103 | this.options.redis.host, 104 | this.options.redis 105 | ); 106 | if (this.options.redis.password) { 107 | client.auth(this.options.redis.password); 108 | } 109 | return client; 110 | }; 111 | 112 | GuysPool.prototype.isBanned = function isBanned(ip, callback) { 113 | this.publish.hget('ban:ips', ip, function(err, value) { 114 | if (err || !value) return callback(false); 115 | callback(true); 116 | }); 117 | }; 118 | 119 | GuysPool.prototype.ban = function ban(guy) { 120 | this.publish.hset('ban:ips', guy.ip, 1); 121 | if (this.io.sockets.sockets[guy.id]) { 122 | this.io.sockets.sockets[guy.id].disconnect(); 123 | } 124 | }; 125 | 126 | GuysPool.prototype.insert = function insert(guy, silent) { 127 | var guyObj = new Guy(this, guy.pid, guy.id, guy.ip); 128 | 129 | if (guy.mode) { 130 | guyObj.mode = guy.mode; 131 | guyObj.pos = guy.pos; 132 | guyObj.text = guy.text; 133 | } 134 | 135 | this.map[guy.id] = guyObj; 136 | if (!this.pmap[guy.pid]) { 137 | this.pmap[guy.pid] = new PMap(this, guy.pid); 138 | } 139 | this.pmap[guy.pid].guys[guy.id] = guyObj; 140 | this.pool.push(guyObj); 141 | 142 | if (!silent) this.notifyEnter(guyObj); 143 | }; 144 | 145 | GuysPool.prototype.notifyEnter = function notifyEnter(guy, bulk) { 146 | this.broadcast('enter', { id: guy.id, pid: this.id }, bulk); 147 | this.broadcast('mode', { id: guy.id, pid: this.id, mode: guy.mode }, bulk); 148 | this.broadcast('move', { 149 | id: guy.id, 150 | pid: this.id, 151 | pos: guy.pos 152 | }, bulk); 153 | if (guy.text) { 154 | this.broadcast('say', { id: guy.id, pid: this.id, text: guy.text }, bulk); 155 | } 156 | }; 157 | 158 | GuysPool.prototype.remove = function remove(guy, silent) { 159 | var index = this.pool.indexOf(guy); 160 | 161 | if (index === -1) return; 162 | 163 | this.pool.splice(index, 1); 164 | delete this.map[guy.id]; 165 | if (this.pmap[guy.pid]) { 166 | delete this.pmap[guy.pid].guys[guy.id]; 167 | } 168 | 169 | if (!silent) this.broadcast('leave', { id: guy.id, pid: guy.pid }); 170 | }; 171 | 172 | GuysPool.prototype.onMessage = function onMessage(channel, data) { 173 | var self = this, 174 | msg = JSON.parse(data); 175 | 176 | if (msg[0] === 'bootstrap') { 177 | if (msg[1] === this.id) return; 178 | var pool = this.pool.map(function(guy) { 179 | return { 180 | id: guy.id, 181 | pid: guy.pid, 182 | ip: guy.ip, 183 | pos: guy.pos, 184 | mode: guy.mode, 185 | text: guy.text 186 | }; 187 | }); 188 | this.publish.publish( 189 | this.options.redis.channel, 190 | JSON.stringify(['bootstrap:reply', msg[1], pool]) 191 | ); 192 | return; 193 | } 194 | 195 | if (msg[0] === 'bootstrap:reply') { 196 | if (msg[1] !== this.id) return; 197 | msg[2].forEach(function(guy) { 198 | if (self.map[guy.id]) return; 199 | self.insert(guy); 200 | }); 201 | return; 202 | } 203 | 204 | if (msg[0] === 'ping') { 205 | if (!this.pmap[msg[1]]) { 206 | this.pmap[msg[1]] = new PMap(this, msg[1]); 207 | } else { 208 | this.pmap[msg[1]].ping(); 209 | } 210 | return; 211 | } 212 | 213 | if (msg[0] === 'bulk') { 214 | msg[1].forEach(function(event) { 215 | var type = event[0], 216 | data = event[1], 217 | guy = self.map[data.id]; 218 | 219 | if (type === 'enter' && !guy) { 220 | self.insert(data, true); 221 | return; 222 | } 223 | if (!guy) return; 224 | 225 | if (type === 'ping') { 226 | guy.ping(); 227 | } else if (type === 'leave') { 228 | self.remove(guy, true); 229 | } else if (type === 'mode') { 230 | guy.mode = data.mode; 231 | } else if (type === 'move') { 232 | if (guy.pos && data.pos && 233 | guy.pos.x !== 0 && guy.pos.y !== 0) { 234 | var dx = guy.pos.x - data.pos.x, 235 | dy = guy.pos.y - data.pos.y, 236 | len = dx*dx + dy*dy; 237 | 238 | if (len > 50 * 50) self.ban(guy); 239 | } 240 | guy.pos = data.pos; 241 | if (!guy.pos|| guy.pos.y < 100 || guy.pos.y > 250) { 242 | self.ban(guy); 243 | } 244 | } else if (type === 'say') { 245 | guy.text += data.text; 246 | if (/penis|ху[йё]|пизд|еба[тн]|gaynode/i.test(guy.text)) { 247 | self.ban(guy); 248 | } 249 | 250 | // Trim text 251 | if (guy.text.length > 32) { 252 | guy.text = guy.text.slice(0, 32) + '...'; 253 | } 254 | } else if (type === 'say:remove') { 255 | guy.text = guy.text.slice(0, -1); 256 | } else if (type === 'say:stop') { 257 | guy.text = ''; 258 | } 259 | }); 260 | } 261 | this.io.sockets.emit(msg[0], msg[1]); 262 | }; 263 | 264 | GuysPool.prototype.manageIo = function manageIo(io) { 265 | var self = this; 266 | 267 | io.sockets.on('connection', function(socket) { 268 | var ip = io.handshaken[socket.id].address.address; 269 | self.isBanned(ip, function(banned) { 270 | if (banned) return socket.disconnect(); 271 | 272 | var bulk = [ ['version', self.version] ]; 273 | self.pool.forEach(function(guy) { 274 | self.notifyEnter(guy, bulk); 275 | }); 276 | socket.emit('bulk', bulk); 277 | 278 | self.broadcast('enter', { id: socket.id, pid: self.id, ip: ip }); 279 | 280 | socket.on('ping', function() { 281 | self.broadcast('ping', { id : socket.id }); 282 | }); 283 | 284 | socket.on('mode', function(mode) { 285 | self.broadcast('mode', { id: socket.id, pid: self.id, mode: mode }); 286 | }); 287 | 288 | socket.on('move', function(pos) { 289 | self.broadcast('move', { 290 | id: socket.id, 291 | pid: self.id, 292 | pos: pos 293 | }); 294 | }); 295 | 296 | socket.on('say', function(text) { 297 | self.broadcast('say', { id: socket.id, pid: self.id, text: text }); 298 | }); 299 | 300 | socket.on('say:remove', function() { 301 | self.broadcast('say:remove', { id: socket.id, pid: self.id }); 302 | }); 303 | 304 | socket.on('say:stop', function() { 305 | self.broadcast('say:stop', { id: socket.id, pid: self.id }); 306 | }); 307 | 308 | socket.on('disconnect', function() { 309 | self.broadcast('leave', { id: socket.id, pid: self.id }); 310 | }); 311 | }); 312 | }); 313 | }; 314 | 315 | GuysPool.prototype.broadcast = function broadcast(type, data, acc) { 316 | if (acc) { 317 | acc.push([type, data]); 318 | } else { 319 | this.publish.publish( 320 | this.options.redis.channel, 321 | JSON.stringify(['bulk', [[type, data]]]) 322 | ); 323 | } 324 | }; 325 | 326 | exports.init = function init(io, options) { 327 | var pool = new GuysPool(io, options); 328 | }; 329 | -------------------------------------------------------------------------------- /public/javascripts/main.js: -------------------------------------------------------------------------------- 1 | !function() { 2 | var requestFrame = window.requestAnimationFrame || 3 | window.mozRequestAnimationFrame || 4 | window.webkitRequestAnimationFrame || 5 | window.msRequestAnimationFrame, 6 | canvas = document.getElementById('drawer'), 7 | ctx = canvas.getContext('2d'), 8 | socket = io.connect(); 9 | 10 | // No request frame in opera 11 | if (!requestFrame) { 12 | requestFrame = function(callback) { 13 | callback(); 14 | }; 15 | } 16 | 17 | ctx.fillStyle = 'black'; 18 | ctx.lineWidth = 1.5; 19 | ctx.textAlign = 'center'; 20 | 21 | var length = { 22 | hand: 4.5, 23 | leg: 6 24 | }; 25 | 26 | function ManPath(head, hands, legs) { 27 | this.position = { 28 | x: 0, 29 | y: 0 30 | }; 31 | 32 | if (head === 'left') { 33 | this.head = this.line([[6, -90], [2, -45]]); 34 | } else { 35 | this.head = this.line([[6, -90], [2, 225]]); 36 | } 37 | this.body = this.line([[8, 90]]); 38 | this.hands = { 39 | left: this.line([ 40 | [length.hand, hands.left[0]], 41 | [length.hand, hands.left[1]] 42 | ]), 43 | right: this.line([ 44 | [length.hand, hands.right[0]], 45 | [length.hand, hands.right[1]] 46 | ]) 47 | }; 48 | this.legs = { 49 | left: this.line( 50 | this.body[this.body.length - 1], 51 | [[length.leg, legs.left[0]], [length.leg, legs.left[1]]] 52 | ), 53 | right: this.line( 54 | this.body[this.body.length - 1], 55 | [[length.leg, legs.right[0]], [length.leg, legs.right[1]]] 56 | ) 57 | }; 58 | 59 | this.lines = [ 60 | this.head, 61 | this.body, 62 | this.hands.left, 63 | this.hands.right, 64 | this.legs.left, 65 | this.legs.right 66 | ]; 67 | 68 | this.height = this.lines.reduce(function(max, line) { 69 | return line.reduce(function(max, point) { 70 | return Math.max(max, point.y); 71 | }, max); 72 | }, 0) - this.lines.reduce(function(min, line) { 73 | return line.reduce(function(min, point) { 74 | return Math.min(min, point.y); 75 | }, min); 76 | }, 0); 77 | }; 78 | 79 | ManPath.prototype.line = function line(from, positions) { 80 | if (positions === undefined) { 81 | positions = from; 82 | from = { x: this.position.x, y: this.position.y }; 83 | } 84 | 85 | var result = [from]; 86 | 87 | positions.forEach(function(pos) { 88 | from = { 89 | x: from.x + pos[0] * Math.cos(pos[1] * Math.PI / 180), 90 | y: from.y + pos[0] * Math.sin(pos[1] * Math.PI / 180) 91 | }; 92 | result.push(from); 93 | }); 94 | 95 | return result; 96 | }; 97 | 98 | ManPath.prototype.draw = function draw() { 99 | var offsetX = this.position.x, 100 | offsetY = this.position.y; 101 | 102 | ctx.save(); 103 | ctx.beginPath(); 104 | this.lines.forEach(function(line) { 105 | line.forEach(function(point, i) { 106 | if (i === 0) { 107 | ctx.moveTo(line[i].x + offsetX, line[i].y + offsetY); 108 | } else { 109 | ctx.lineTo(line[i].x + offsetX, line[i].y + offsetY); 110 | } 111 | }); 112 | }); 113 | ctx.stroke(); 114 | ctx.restore(); 115 | }; 116 | 117 | function createManPaths(head, hands, legs) { 118 | var result = []; 119 | for (var i = 0; i < hands.left.length; i++) { 120 | result.push(new ManPath(head, { 121 | left: hands.left[i], 122 | right: hands.right[i] 123 | }, { 124 | left: legs.left[i], 125 | right: legs.right[i] 126 | })); 127 | } 128 | return result; 129 | } 130 | 131 | function Man() { 132 | this.standby = createManPaths('left', { 133 | left: [[117, 100]], 134 | right: [[63, 80]] 135 | }, { 136 | left: [[100, 95]], 137 | right: [[80, 85]] 138 | }); 139 | this.walkLeft = createManPaths('left', { 140 | left: [[95, 155], [95, 125], [90, 90], [75, 75], [65, 65]], 141 | right: [[65, 65], [75, 75], [90, 90], [95, 125], [95, 150]] 142 | }, { 143 | left: [[105, 100], [97, 95], [90, 90], [86, 78], [83, 70]], 144 | right: [[86, 65], [96, 48], [106, 55], [116, 91], [110, 110]] 145 | }); 146 | this.walkRight = createManPaths('right', { 147 | left: [[115, 115], [105, 105], [90, 90], [85, 55], [85, 30]], 148 | right: [[85, 30], [85, 55], [90, 90], [105, 105], [115, 115]] 149 | }, { 150 | left: [[94, 115], [84, 132], [74, 125], [64, 89], [70, 70]], 151 | right: [[75, 80], [83, 85], [90, 90], [94, 102], [97, 110]] 152 | }); 153 | 154 | this.text = ''; 155 | this.textTimeout = undefined; 156 | 157 | this.mode = undefined; 158 | this._active = undefined; 159 | this._current = undefined; 160 | this._index = 0; 161 | this.position = { x: 0, y: 0 }; 162 | this.place = 'basement'; 163 | }; 164 | 165 | Man.prototype.activate = function activate(mode) { 166 | if (!this[mode]) return; 167 | 168 | this._active = this[mode]; 169 | this.mode = mode; 170 | this._index = 0; 171 | this.tick(); 172 | if (this === man) socket.emit('mode', mode); 173 | redraw(); 174 | }; 175 | 176 | Man.prototype.move = function move(pos) { 177 | if (pos) { 178 | if (this.position) { 179 | this.position.x = pos.x; 180 | this.position.y = pos.y; 181 | } else { 182 | this.position = pos; 183 | } 184 | } 185 | if (this === man) { 186 | socket.emit('move', { x: this.position.x, y: this.position.y }); 187 | } 188 | redraw(); 189 | }; 190 | 191 | Man.prototype.add = function add(vector) { 192 | this.position.x = this.position.x + vector.x; 193 | this.position.y = this.position.y + vector.y; 194 | if (this === man) { 195 | socket.emit('move', { x: this.position.x, y: this.position.y }); 196 | } 197 | redraw(); 198 | }; 199 | 200 | Man.prototype.say = function say(text) { 201 | this.text += text; 202 | if (this.text.length > 32) { 203 | this.text = this.text.slice(0, 32) + '...'; 204 | } 205 | 206 | var self = this; 207 | clearTimeout(this.textTimeout); 208 | this.textTimeout = setTimeout(function() { 209 | self.stopSaying(); 210 | }, 8500); 211 | 212 | if (this === man) socket.emit('say', text); 213 | redraw(); 214 | }; 215 | 216 | Man.prototype.backspaceSaying = function backspaceSaying() { 217 | this.text = this.text.slice(0, -1); 218 | clearTimeout(this.textTimeout); 219 | if (this === man) socket.emit('say:remove'); 220 | redraw(); 221 | }; 222 | 223 | Man.prototype.stopSaying = function stopSaying() { 224 | this.text = ''; 225 | clearTimeout(this.textTimeout); 226 | if (this === man) socket.emit('say:stop'); 227 | redraw(); 228 | }; 229 | 230 | Man.prototype.tick = function tick() { 231 | if (!this.mode) this.activate('standby'); 232 | 233 | if (this._active) { 234 | this._index = (this._index + 1) % this._active.length; 235 | } 236 | this._current = this._active[this._index]; 237 | if (this.mode !== 'standby') { 238 | redraw(); 239 | } 240 | }; 241 | 242 | Man.prototype.draw = function draw() { 243 | if (!this._current) return; 244 | var current = this._current; 245 | 246 | current.position.x = this.position.x; 247 | current.position.y = this.position.y - current.height; 248 | current.draw(); 249 | 250 | if (this.text) { 251 | ctx.fillText(this.text, this.position.x, this.position.y - 40); 252 | } 253 | }; 254 | 255 | var man = new Man(), 256 | ghosts = [], 257 | ghostsMap = {}; 258 | 259 | man.move({ x: 40, y: 235 }); 260 | 261 | var i = 0; 262 | setInterval(function() { 263 | if (i++ % 2 === 0) { 264 | man.tick(); 265 | ghosts.forEach(function(ghost) { 266 | ghost.tick(); 267 | }); 268 | } 269 | 270 | if (man.place === 'basement') { 271 | if (man.position.x > 79) { 272 | if (man.position.y - 1.7 >= 123) { 273 | man.add({ x: 0, y: -1.7 }); 274 | return; 275 | } else { 276 | man.place = 'lift'; 277 | man.position.y = 123; 278 | man.move(); 279 | } 280 | } else if (man.position.x <= 22 && man.mode === 'walkLeft') { 281 | return; 282 | } 283 | } else if (man.place === 'lift') { 284 | if (man.position.y <= 124) { 285 | if (man.position.x > 99) { 286 | man.place = 'roof'; 287 | } else if (man.position.x <= 80 && man.mode === 'walkLeft') { 288 | return; 289 | } 290 | } 291 | if (man.position.y >= 209) { 292 | if (man.position.x < 75) { 293 | man.place = 'basement'; 294 | } else if (man.position.x > 80 && man.mode === 'walkRight') { 295 | return; 296 | } 297 | } 298 | } else if (man.place === 'roof') { 299 | if (man.position.x <= 82) { 300 | if (man.position.y + 1.7 <= 235) { 301 | man.add({ x: 0, y: 1.7 }); 302 | return; 303 | } else { 304 | man.place = 'lift'; 305 | man.position.y = 235; 306 | man.move(); 307 | } 308 | } else if (man.position.x >= 474 && man.mode === 'walkRight') { 309 | showInfo(); 310 | return; 311 | } 312 | } 313 | 314 | if (man.mode === 'walkLeft') { 315 | man.add({ x: -0.75, y: 0 }); 316 | } else if (man.mode === 'walkRight') { 317 | man.add({ x: 0.75, y: 0 }); 318 | } 319 | }, 20); 320 | 321 | function redraw() { 322 | requestFrame(function() { 323 | ctx.clearRect(0, 0, canvas.width, canvas.height); 324 | 325 | man.draw(); 326 | ghosts.forEach(function(ghost) { 327 | ghost.draw(); 328 | }); 329 | }, canvas); 330 | }; 331 | 332 | var shown = false; 333 | function showInfo() { 334 | if (shown) return; 335 | shown = true; 336 | 337 | var contacts = document.getElementById('contacts'); 338 | contacts.className = 'visible'; 339 | setTimeout(function() { 340 | contacts.className = 'visible full'; 341 | }, 3); 342 | } 343 | 344 | // Controlling our guy 345 | 346 | var keyMap = { 347 | 37: 'left', 348 | 39: 'right' 349 | }; 350 | 351 | window.addEventListener('keydown', function(e) { 352 | // Process backspace 353 | if (e.keyCode === 8) { 354 | e.preventDefault(); 355 | man.backspaceSaying(); 356 | return false; 357 | } 358 | 359 | var key = keyMap[e.keyCode]; 360 | 361 | if (!key) return; 362 | 363 | if (man.mode !== 'standby') return; 364 | if (key === 'left') { 365 | man.activate('walkLeft'); 366 | } else if (key === 'right') { 367 | man.activate('walkRight'); 368 | } 369 | }, true); 370 | 371 | window.addEventListener('keyup', function(e) { 372 | var key = keyMap[e.keyCode]; 373 | if (!key) return; 374 | 375 | if (key === 'left' && man.mode === 'walkLeft' || 376 | key === 'right' && man.mode === 'walkRight') { 377 | man.activate('standby'); 378 | } 379 | }, true); 380 | 381 | // Chat messaging 382 | window.addEventListener('keypress', function(e) { 383 | if (e.keyCode === 13) return man.stopSaying(); 384 | var char = String.fromCharCode(e.charCode); 385 | if (!char || !e.charCode) return; 386 | 387 | man.say(char); 388 | }, true); 389 | 390 | // Displaying ghosts 391 | function onGuyEnter(guy) { 392 | // Ignore myself 393 | if (guy.id === socket.socket.sessionid) return; 394 | if (ghostsMap[guy.id]) return; 395 | ghosts.push(ghostsMap[guy.id] = new Man()); 396 | }; 397 | 398 | function onGuyLeave(guy) { 399 | // I was kicked - refresh page 400 | if (guy.id === socket.socket.sessionid) { 401 | location.reload(true); 402 | return; 403 | } 404 | 405 | if (!ghostsMap[guy.id]) return; 406 | var ghost = ghostsMap[guy.id], 407 | index = ghosts.indexOf(ghost); 408 | 409 | if (index === -1) return; 410 | 411 | delete ghostsMap[guy.id]; 412 | 413 | if (index === -1) return; 414 | ghosts.splice(index, 1); 415 | }; 416 | 417 | function onGuyMode(guy) { 418 | if (!ghostsMap[guy.id]) return; 419 | ghostsMap[guy.id].activate(guy.mode); 420 | }; 421 | 422 | function onGuyMove(guy) { 423 | if (!ghostsMap[guy.id]) return; 424 | ghostsMap[guy.id].move(guy.pos); 425 | }; 426 | 427 | function onGuySay(guy) { 428 | if (!ghostsMap[guy.id]) return; 429 | ghostsMap[guy.id].say(guy.text); 430 | }; 431 | 432 | function onGuyBackspaceSaying(guy) { 433 | if (!ghostsMap[guy.id]) return; 434 | ghostsMap[guy.id].backspaceSaying(); 435 | }; 436 | 437 | function onGuyStopSaying(guy) { 438 | if (!ghostsMap[guy.id]) return; 439 | ghostsMap[guy.id].stopSaying(); 440 | }; 441 | 442 | var serverVersion; 443 | socket.on('bulk', function(bulk) { 444 | bulk.forEach(function(msg) { 445 | var type = msg[0], 446 | data = msg[1]; 447 | 448 | if (type === 'version') { 449 | if (serverVersion === undefined) { 450 | serverVersion = data; 451 | } else if (serverVersion != data) { 452 | // Update was deployed 453 | location.reload(true); 454 | } 455 | } 456 | if (type === 'enter') return onGuyEnter(data); 457 | if (type === 'leave') return onGuyLeave(data); 458 | if (type === 'mode') return onGuyMode(data); 459 | if (type === 'move') return onGuyMove(data); 460 | if (type === 'say') return onGuySay(data); 461 | if (type === 'say:remove') return onGuyBackspaceSaying(data); 462 | if (type === 'say:stop') return onGuyStopSaying(data); 463 | }); 464 | }); 465 | 466 | setInterval(function() { 467 | socket.emit('ping'); 468 | }, 3000); 469 | }(); 470 | --------------------------------------------------------------------------------