├── .gitignore ├── README.md ├── package.json └── server ├── static ├── index.html ├── style.css └── js.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paint Online 2 | 3 | [Demo](http://moyuyc.xyz:4001/) 4 | 5 | npm install 6 | 7 | node server/server.js 8 | 9 | http://localhost:4001/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paint_online", 3 | "version": "1.0.0", 4 | "description": "基于socket.io 实现的实时画板与实时聊天", 5 | "main": "server/server.js", 6 | "dependencies": { 7 | "socket.io": "^1.4.6" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+ssh://git@github.com/moyuyc/paint_online.git" 15 | }, 16 | "keywords": [ 17 | "node", 18 | "socket.io" 19 | ], 20 | "author": "Moyu", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/moyuyc/paint_online/issues" 24 | }, 25 | "homepage": "https://github.com/moyuyc/paint_online#readme" 26 | } 27 | -------------------------------------------------------------------------------- /server/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Paint Online 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Paint Online

15 |
16 |
17 | 18 | Sorry, Your Browser don't support canvas of Html5. 19 | 20 |
21 |
22 | Colors 23 | 24 |
25 |
26 | Width 27 | 28 | 1 29 | 按住Ctrl可以移动自己的痕迹 30 |
31 | 下载 32 | 清除我的痕迹 33 |
34 |
35 |
36 |
37 |
38 |
39 |

Messages

40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /server/static/style.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | padding: 0; 4 | font-family: 微软雅黑Arial, Arial, sans-serif; 5 | } 6 | body{ 7 | min-width: 1000px; 8 | touch-action: none; 9 | } 10 | canvas{ 11 | cursor: crosshair; 12 | } 13 | .text-tip{ 14 | color: crimson; 15 | } 16 | .movable{ 17 | cursor: move; 18 | } 19 | h1,h2,h3,h4,h5,h6{ 20 | text-align: center; 21 | margin: 10px 4px 20px; 22 | } 23 | .container{ 24 | width: 98%; 25 | margin: 0 auto; 26 | } 27 | .triangle-r{ 28 | display: inline-block; 29 | border-style: solid; 30 | border-color: transparent transparent transparent cornflowerblue ; 31 | border-width:13px; 32 | height: 0px; 33 | width: 0px; 34 | overflow: hidden; 35 | } 36 | .col-7{ 37 | width: 68%; 38 | float: left; 39 | margin-left: 20px; 40 | } 41 | .col-3{ 42 | margin-left: 20px; 43 | width: 25%; 44 | float: left; 45 | } 46 | .row:after{ 47 | content: '.'; 48 | height: 0; 49 | display: block; 50 | visibility: hidden; 51 | clear: both; 52 | } 53 | .text-blue { 54 | color: cornflowerblue; 55 | } 56 | .fl{ 57 | float: left; 58 | } 59 | .mid{ 60 | vertical-align: middle; 61 | } 62 | .active{ 63 | border: 3px solid fuchsia !important; 64 | } 65 | 66 | .rect{ 67 | display: inline-block; 68 | width: 16px; 69 | height: 16px; 70 | border: 3px solid black; 71 | text-align: center; 72 | margin:0 2px 0; 73 | cursor: pointer; 74 | } 75 | .ctl-row{ 76 | border: 1px solid darkgreen; 77 | background-color: beige; 78 | padding-top:3px; 79 | 80 | border-radius: 6px; 81 | margin: 2px 0 2px; 82 | } 83 | 84 | .box-sh{ 85 | box-shadow: 0px 0px 8px 3px #bbb; 86 | } 87 | .btn{ 88 | display: inline-block; 89 | border: none; 90 | cursor: pointer; 91 | padding: 5px; 92 | margin: 4px; 93 | border-radius: 3px; 94 | } 95 | .fr{ 96 | float: right; 97 | } 98 | a{ 99 | text-decoration: none; 100 | font-size: 14px; 101 | } 102 | .btn-blue{ 103 | background-color: cornflowerblue; 104 | color: azure; 105 | } 106 | .btn:hover{ 107 | box-shadow: 0px 0px 2px 2px #bbb; 108 | } 109 | 110 | #msg{ 111 | overflow-y: scroll; 112 | padding: 0 10px 0; 113 | /*width: 100%;*/ 114 | min-height: 400px; 115 | max-height: 600px; 116 | } 117 | #msg p{ 118 | white-space:nowrap; 119 | } 120 | input[type=range]{cursor: pointer;} 121 | input[type=text]{ 122 | height: 20px; 123 | font-size: 16px; 124 | margin: 5px; 125 | width: 90%; 126 | } -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Yc on 2016/5/21. 3 | */ 4 | var httpd = require('http').createServer(handler); 5 | var io = require('socket.io').listen(httpd); 6 | var fs = require('fs'); 7 | var port = 80; 8 | httpd.listen(port); 9 | console.log('http://localhost:'+port); 10 | function handler(req,res) { 11 | fs.readFile(__dirname+'/static/'+(req.url==='/'?'index.html':req.url), 12 | function (err,data) { 13 | if(err){ 14 | res.writeHead(500); 15 | return res.end('Error loading index.html'); 16 | } 17 | res.writeHead(200); 18 | res.end(data); 19 | } 20 | ); 21 | 22 | // var stream = fs.createReadStream(__dirname+'/static/'+(req.url==='/'?'index.html':req.url)); 23 | // if(stream) 24 | // stream.pipe(res); 25 | } 26 | var paths = (function () { 27 | var _paths = {}; 28 | return { 29 | set :function (key,paths) { 30 | _paths[key] = paths; 31 | }, 32 | add:function (key,pts) { 33 | _paths[key]=_paths[key]||[]; 34 | _paths[key].push(pts); 35 | }, 36 | get:function (key) { 37 | return _paths[key]; 38 | }, 39 | remove:function (key) { 40 | delete _paths[key]; 41 | }, 42 | clear:function () { 43 | _paths = []; 44 | }, 45 | toJSON:function () { 46 | var keys = Object.keys(_paths),all=[]; 47 | for(var i in keys){ 48 | var key = keys[i]; 49 | all = all.concat(_paths[key]); 50 | } 51 | return all; 52 | } 53 | } 54 | })(); 55 | function doCmd(msg,socket) { 56 | if(msg[0]==='#'){ 57 | var msg = msg.substring(1), 58 | sockets = (function(s){ 59 | var a = [] 60 | for(var k in s) 61 | a.push(s[k]); 62 | return a; 63 | })(socket.server.sockets.sockets); 64 | switch (msg) { 65 | case 'show paths': 66 | socket.emit('cmd',JSON.stringify(paths)); 67 | socket.emit('server msg','指令操作成功!'); 68 | break; 69 | case 'show users': 70 | socket.emit('cmd',JSON.stringify(sockets.map(x=>x=x.name))); 71 | socket.emit('server msg','指令操作成功!'); 72 | break; 73 | case 'clear paths': 74 | paths.clear(); 75 | socket.emit('server msg','指令操作成功!'); 76 | socket.broadcast.emit('paint paths',JSON.stringify(paths)); 77 | socket.emit('paint paths',JSON.stringify(paths)); 78 | break; 79 | default: return false; 80 | } 81 | return true; 82 | }else{ 83 | return false; 84 | } 85 | } 86 | 87 | function escapeHTML(data) { 88 | var s = ''; 89 | for(var i = 0 ;i': 99 | d = '>'; break; 100 | case ' ': 101 | d = ' '; break; 102 | default: 103 | s+=d; 104 | } 105 | } 106 | return s; 107 | } 108 | io.sockets.on('connection',function (socket) { 109 | socket.on('login',function (name) { 110 | socket.name = name || socket.id.substring(2); 111 | console.log('new user',new Date().format('yyyy-MM-dd hh:mm:ss'),socket.name); 112 | socket.emit('server msg','欢迎, '+socket.name+' !'); 113 | socket.broadcast.emit('server msg','欢迎, '+socket.name+' !'); 114 | socket.emit('paint paths',JSON.stringify(paths)); 115 | 116 | socket.on('client msg',function (msg) { 117 | if(!doCmd(msg,socket)) { 118 | msg = escapeHTML(msg); 119 | var date = new Date().format('yyyy-MM-dd hh:mm:ss'); 120 | socket.emit('server msg',date+'
'+ socket.name + ' 说: ' + msg); 121 | socket.broadcast.emit('server msg',date+'
'+ socket.name + ' 说: ' + msg); 122 | } 123 | }); 124 | socket.on('disconnect',function () { 125 | //paths.remove(this.id); 126 | socket.broadcast.emit('server msg','拜, '+socket.name +'。'); 127 | }); 128 | socket.on('remove paint',function () { 129 | paths.remove(this.id); 130 | socket.emit('paint paths',JSON.stringify(paths)); 131 | socket.broadcast.emit('paint paths',JSON.stringify(paths)); 132 | }); 133 | socket.on('move my paint',function (x,y) { 134 | var _paths = paths.get(socket.id) || []; 135 | _paths.forEach(ele=>{ 136 | ele.pts.forEach(v=>{ 137 | v.x+=x; 138 | v.y+=y; 139 | }); 140 | }); 141 | paths.set(socket.id,_paths); 142 | socket.emit('paint paths',JSON.stringify(paths)); 143 | socket.broadcast.emit('paint paths',JSON.stringify(paths)); 144 | }); 145 | socket.on('paint',function (data) { 146 | data = JSON.parse(data); 147 | var pts = data.data; 148 | switch (data.status){ 149 | case 'ing' : 150 | socket.broadcast.emit('paint pts',JSON.stringify(pts)); 151 | break; 152 | case 'end' : 153 | socket.broadcast.emit('paint pts',JSON.stringify(pts)); 154 | paths.add(this.id,pts); 155 | break; 156 | } 157 | }); 158 | socket.on('repaint',function () { 159 | socket.emit('paint paths',JSON.stringify(paths)); 160 | }) 161 | }); 162 | socket.emit('login'); 163 | }) 164 | 165 | Date.prototype.format = function (fmt) { //author: meizz 166 | var o = { 167 | "M+": this.getMonth() + 1, //月份 168 | "d+": this.getDate(), //日 169 | "h+": this.getHours(), //小时 170 | "m+": this.getMinutes(), //分 171 | "s+": this.getSeconds(), //秒 172 | "q+": Math.floor((this.getMonth() + 3) / 3), //季度 173 | "S": this.getMilliseconds() //毫秒 174 | }; 175 | if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); 176 | for (var k in o) 177 | if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); 178 | return fmt; 179 | } -------------------------------------------------------------------------------- /server/static/js.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Yc on 2016/5/17. 3 | */ 4 | 5 | var canvas = document.getElementsByTagName('canvas')[0], 6 | ctx = canvas.getContext('2d'), 7 | msg = document.getElementById('msg'), 8 | ranger = document.getElementById('ranger'), 9 | colors = document.getElementById('colors'); 10 | 11 | var input = document.getElementById('input-msg'); 12 | var socket = io.connect(); 13 | socket.on('server msg',function (data) { 14 | var ele = document.createElement('p'); 15 | ele.innerHTML = data; 16 | msg.appendChild(ele); 17 | msg.scrollTop = msg.scrollHeight; 18 | }) 19 | socket.on('login',function () { 20 | socket.emit('login',prompt('输入你的姓名')); 21 | }); 22 | 23 | socket.on('paint paths',function (paths) { 24 | paths = JSON.parse(paths) 25 | ctx.clearRect(0,0,canvas.width,canvas.height); 26 | for(var k in paths) 27 | Ctl.drawPts(ctx, paths[k]); 28 | }) 29 | socket.on('paint pts',function (pts) { 30 | //canvas.paths = paths; 31 | pts = JSON.parse(pts) 32 | if(!pts) return; 33 | Ctl.drawPts(ctx, pts); 34 | }); 35 | socket.on('cmd',function (data) { 36 | console.log(JSON.parse(data)); 37 | }) 38 | 39 | window.onload = function () { 40 | Ctl.init(); 41 | function resize() { 42 | canvas.width = canvas.parentElement.clientWidth; 43 | canvas.paths = canvas.pts = []; 44 | socket.emit('repaint'); 45 | } 46 | this.addEventListener('resize',resize); 47 | resize(); 48 | input.onkeydown = function (e) { 49 | if(e.keyCode === 13 && this.value!=''){ 50 | socket.emit('client msg',this.value); 51 | this.value = ''; 52 | } 53 | } 54 | } 55 | 56 | function bind(ele,type,fn) { 57 | fn = fn.bind(ele); 58 | ele[type] = fn; 59 | ele.addEventListener(type,fn); 60 | } 61 | bind(canvas,'mousemove',function (e) { 62 | if(e.buttons === 1) { 63 | var x = e.offsetX, y = e.offsetY; 64 | if(e.ctrlKey){ 65 | this.classList.add('movable'); 66 | if(this.mouseDown) 67 | socket.emit('move my paint',x-this.mouseDown.x,y-this.mouseDown.y); 68 | this.mouseDown={x:e.offsetX,y:e.offsetY}; 69 | }else { 70 | Ctl.addPos(x, y); 71 | Ctl.drawPts(ctx, this.pts); 72 | socket.emit('paint', JSON.stringify({data: new Path(this.pts), status: 'ing'})) 73 | } 74 | } 75 | }); 76 | // bind(canvas,'touchstart',function (e) { 77 | // var x = e.changedTouches[0].clientX, y = e.changedTouches[0].clientY; 78 | // Ctl.addPos(x, y); 79 | // Ctl.drawPts(ctx, this.pts); 80 | // socket.emit('paint', JSON.stringify({data: new Path(this.pts), status: 'ing'})) 81 | // }) 82 | // bind(canvas,'touchmove',function (e) { 83 | // var x = e.changedTouches[0].clientX, y = e.changedTouches[0].clientY; 84 | // Ctl.addPos(x, y); 85 | // Ctl.drawPts(ctx, this.pts); 86 | // socket.emit('paint', JSON.stringify({data: new Path(this.pts), status: 'ing'})) 87 | // }) 88 | // bind(canvas,'touchend',function (e) { 89 | // var x = e.changedTouches[0].clientX, y = e.changedTouches[0].clientY; 90 | // Ctl.addPos(x, y); 91 | // Ctl.addPath(this.pts); 92 | // socket.emit('paint',JSON.stringify({data:new Path(this.pts),status:'end'})) 93 | // Ctl.clearPos(); 94 | // }) 95 | bind(canvas,'mouseup',function (e) { 96 | this.classList.remove('movable'); 97 | if(!this.mouseDown) { 98 | var x = e.offsetX, y = e.offsetY; 99 | Ctl.addPos(x, y); 100 | } 101 | Ctl.addPath(this.pts); 102 | socket.emit('paint',JSON.stringify({data:new Path(this.pts),status:'end'})) 103 | Ctl.clearPos(); 104 | delete this.mouseDown; 105 | }) 106 | 107 | bind(canvas,'mousedown',function (e) { 108 | var x = e.offsetX,y = e.offsetY; 109 | this.mouseDown={x:e.offsetX,y:e.offsetY}; 110 | Ctl.clearPos(); 111 | Ctl.addPos(x,y); 112 | }); 113 | colors.addEventListener('click',function (e) { 114 | var t = e.target; 115 | if(t.classList.contains('rect')){ 116 | Array.prototype.slice.call(this.getElementsByClassName('active')) 117 | .forEach(v=>v.classList.remove('active')); 118 | t.classList.add('active'); 119 | Ctl.setColor(t.style.backgroundColor); 120 | } 121 | }); 122 | ranger.addEventListener('change',function (e) { 123 | this.nextElementSibling.innerText = this.value; 124 | Ctl.setLw(this.value); 125 | }); 126 | 127 | // Controller 128 | Ctl = { 129 | drawPts: function (ctx,pts) { 130 | if(pts instanceof Path || pts.pts){ 131 | var color = pts.color,lw = pts.lw; 132 | pts = pts.pts; 133 | } 134 | var p1 = pts[0]; 135 | ctx.save(); 136 | ctx.beginPath(); 137 | ctx.moveTo(p1.x, p1.y); 138 | pts.slice(1).forEach(v=>{ 139 | ctx.lineTo(v.x,v.y); 140 | }); 141 | ctx.lineWidth = lw || canvas.lw 142 | ctx.strokeStyle = color || canvas.color; 143 | ctx.stroke(); 144 | ctx.restore(); 145 | }, 146 | init : function () { 147 | canvas.paths=[]; 148 | canvas.pts=[]; 149 | canvas.color = 'black'; 150 | canvas.lw = 1; 151 | for(var i=0;i<20;i++) 152 | this.addColor(); 153 | }, 154 | setLw(lw){ 155 | canvas.lw = lw; 156 | }, 157 | setColor(c){ 158 | canvas.color = c; 159 | }, 160 | addPath : function (pts) { 161 | canvas.paths.push(new Path(pts,canvas.lw,canvas.color)); 162 | }, 163 | addPos : function (x,y) { 164 | canvas.pts.push(new Pos(x,y)); 165 | }, 166 | clearPos : function () { 167 | canvas.pts = [] 168 | }, 169 | addColor : function (active) { 170 | var rect = document.createElement('div'),r = this.random; 171 | rect.className = 'rect'; 172 | if(active) 173 | rect.className+=' active'; 174 | rect.style.backgroundColor = 'rgb('+[r(256),r(256),r(256)].join(',')+')'; 175 | colors.appendChild(rect); 176 | }, 177 | random : function (b) { 178 | return Math.floor(Math.random()*b); 179 | } 180 | }; 181 | 182 | // webSocket 183 | /* 184 | var ws = WS({ 185 | path:'ws', 186 | onOpen:function (e) { 187 | alert('OK'); 188 | }, 189 | onError:function (e) { 190 | // alert(e.message) 191 | alert('Error'); 192 | }, 193 | onReceive:function (data,t) { 194 | 195 | }, 196 | onClose:function (e) { 197 | alert('Close'); 198 | } 199 | });*/ 200 | 201 | 202 | 203 | // model 204 | 205 | function Pos(x,y) { 206 | this.x=x;this.y=y; 207 | } 208 | 209 | function Path(pts,lw,color) { 210 | this.pts = pts; 211 | this.lw = lw || canvas.lw; 212 | this.color = color || canvas.color; 213 | } 214 | 215 | --------------------------------------------------------------------------------