├── .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 |
20 |
21 |
22 | Colors
23 |
24 |
25 |
26 |
Width
27 |
28 |
1
29 |
按住Ctrl可以移动自己的痕迹
30 |
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 |
--------------------------------------------------------------------------------