iT邦幫忙

DAY 16
4

且戰且走HTML5系列 第 16

且戰且走HTML5(16) Canvas與Websocket整合

今天先思考一下,要把Canvas與WebSocket整合的話,需要怎樣的技術。順便調整一下之前寫好的Chat伺服器程式,以他為base來接上WebSocket。
目前想到大概有兩個重點需要考慮:

第一步要克服的,是需要記錄在Canvas上面繪圖的「動作」,把這些動作透過WebSocket傳送,然後在他人的瀏覽器中畫出來。另外,也需要制定好一個資料傳輸的格式,包含繪圖的類型與資料等等。

第二步要克服的,則是多人協同作業中,一定會碰到的「資料同步」問題。由於同時有本地端的繪圖動作與透過WebSocket傳來的繪圖動作,如果動作本身不是Atomic的,就可能會互相干擾。不過對於白板這個應用的模型來說,通常問題主要是繪圖的順序與2D Context全域屬性的控制等。這不像多人協同編輯文件或程式複雜,但是還是需要仔細研究是否會有問題。

另外一個問題是測試。要一個人同時操作兩個瀏覽器是有困難的,但是不想辦法測試,就很難找出多人協同作業時的潛在問題。這部份有兩種可能的作法,一個是利用Selenium,另外一個是直接用WebSocket送測試資料,然後本地端也同時操作。

先不管是否會有同步問題,把Socket.IO接上來看一看吧。

不過要一次做好所有繪圖功能好像有點多,就先調整一下之前寫好的聊天室。因為之後可能會加上更多的外部檔案,只使用寫死的檔案規則來對應就有點麻煩。為了簡化,先把跟Socket.IO搭配的http伺服器調整一下,讓他根據簡單的方法與檔案系統對應,並且加上檔案快取,來改進效率:

 var fs = require('fs'),
 url = require('url'),
 mime = require('mime'),
 index = '/test839.html',
 cache = {},
 app = require('http').createServer(function(req, res) {
 	var resource = url.parse(req.url).pathname;
 	if(typeof cache[resource] === 'undefined') {
 		fs.stat(__dirname + resource, function(err, stat) {
 			if(err) {
 				if(typeof cache[index] === 'undefined') {
 					fs.readFile(__dirname+index, function(err, data) {
 						if(err) {
 							console.log('cond 1');
 							res.writeHead(500);
 							res.end();
 						} else {
 							cache[index] = data;
 							res.setHeader('Content-Type', 'text/html');
 							res.writeHead(200);
 							res.end(data);
 						}
 					})
 				} else {
 					res.setHeader('Content-Type', 'text/html');
 					res.writeHead(200);
 					res.end(cache[index]);
 				}
 			} else {
 				fs.readFile(__dirname + resource, function(err, data) {
 					if(err) {
 						if(typeof cache[index] === 'undefined') {
 							fs.readFile(__dirname+index, function(err, data) {
 								if(err) {
 									res.writeHead(500);
 									res.end();
 								} else {
 									cache[index] = data;
 									res.setHeader('Content-Type', 'text/html');
 									res.writeHead(200);
 									res.end(data);
 								}
 							});
 						} else {
 							res.setHeader('Content-Type', 'text/html');
 							res.writeHead(200);
 							res.end(cache[index]);
 						}
 					} else {
 						cache[resource] = data;
 						var type = mime.lookup(resource);
 						res.setHeader('Content-Type', type);
 						res.writeHead(200);
 						res.end(data);
 					}
 				})
 			}
 		})
 	} else {
 		var type = mime.lookup(resource);
 		res.setHeader('Content-Type', type);
 		res.writeHead(200);
 		res.end(cache[resource]);
 	}
 }),
 io = require('socket.io').listen(app)
 ......

然後順便在Canvas繪圖加上橡皮擦:

 ......
 	var eraser = {
 		type: 'eraser',
 		run: function(x, y, x1, y1) {
 			/*this.ctx.save();
 			this.ctx.strokeStyle = '#FFFFFF';
 			this.ctx.lineWidth = '16';
 			this.ctx.beginPath();
 			this.ctx.moveTo(x,y);
 			this.ctx.lineTo(x1,y1);
 			this.ctx.closePath();
 			this.ctx.stroke();
 			this.ctx.restore();*/
 			this.ctx.clearRect(x-8, y-8, 16, 16);
 			var d = Math.abs(x1-x);
 			var xt = x, yt = y;
 			for(var i=0; i<d; i++) {
 				xt += (x1-x)/d;
 				yt += (y1-y)/d;
 				this.ctx.clearRect(xt-8, Math.round(yt)-8, 16, 16);
 			}
 			this.ctx.clearRect(x1-8, y1-8, 16, 16);
 		},
 		handlers: {
 			mousedown: function(e) {
 				if(!this.drawing) {
 					$(e.currentTarget).data('cursor', $(e.currentTarget).css('cursor'));
 					$(e.currentTarget).css('cursor', 'pointer');
 					this.drawing = true;
 					var offset = $(e.currentTarget).offset()
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e) {
 				if(this.drawing) {
 					var old = this.queue.shift();
 					var offset = $(e.currentTarget).offset();
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(old[0],old[1],x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mouseup: function(e) {
 				if(this.drawing) {
 					$(e.currentTarget).css('cursor', $(e.currentTarget).data('cursor'));
 					this.dropCanvas();
 					var old = this.queue.shift();
 					var offset = $(e.currentTarget).offset();
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(old[0], old[1], x, y);
 					this.drawing = false;
 				}
 			},
 			mouseleave: function(e) {
 				if(this.drawing) {
 					$(e.currentTarget).css('cursor', $(e.currentTarget).data('cursor'));
 					this.dropCanvas();
 					var old = this.queue.shift();
 					var offset = $(e.currentTarget).offset();
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(old[0], old[1], x, y);
 					this.drawing = false;
 				}
 			}
 		}
 	}
 	tool.regist(eraser);
 ......

除了繪圖方法之外,其實橡皮擦跟塗鴉幾乎是一樣的。至於繪圖方法的實作,最簡單的方法是畫粗粗的白線,不過這樣做的話,如果Canvas有背景色就會露餡XD,所以改成用clearRect來實作。不過用clearRect的話會有一個問題,就是滑鼠觸發mousemove時,座標其實不是連續的,只在上一個座標跟目前的座標呼叫clearRect,中間有可能會有地方沒清除到。簡單的解法,是使用跛腳的畫直線方法,沿著兩個座標間的直線一路清過來。(畫直線最簡單的就是Bresenham法,不過我用的是偷懶法,沒有用斜率做條件,也沒有乖乖求出y座標)

還是抓個圖來看看:

至於橡皮擦的尖端是方的,就不要挑了,畢竟是clearRect。如果要做漂亮的橡皮擦,目前大概只有直接抓Pixel來算,這樣太花時間...

基本架構調整過,接下來伺服器就還是使用node.js配合Socket.IO(或是ws,如果需要透過WebSocket傳送binary資料的話),來開始結合Canvas與WebSocket。目前構想是先實作出透過WebSocket傳送資料然後透過繪圖方法來繪圖,這樣的好處是可以透過WebSocket送測試資料來做測試。之後再來解決可能存在的多人同步的問題。


上一篇
且戰且走HTML5(15) Canvas基本繪圖-架構調整
下一篇
且戰且走HTML5(17) 將Canvas繪圖進一步抽象
系列文
且戰且走HTML530

1 則留言

我要留言

立即登入留言