今天先思考一下,要把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有背景色就會露餡,所以改成用clearRect來實作。不過用clearRect的話會有一個問題,就是滑鼠觸發mousemove時,座標其實不是連續的,只在上一個座標跟目前的座標呼叫clearRect,中間有可能會有地方沒清除到。簡單的解法,是使用跛腳的畫直線方法,沿著兩個座標間的直線一路清過來。(畫直線最簡單的就是Bresenham法,不過我用的是偷懶法,沒有用斜率做條件,也沒有乖乖求出y座標)
還是抓個圖來看看:
至於橡皮擦的尖端是方的,就不要挑了,畢竟是clearRect。如果要做漂亮的橡皮擦,目前大概只有直接抓Pixel來算,這樣太花時間...
基本架構調整過,接下來伺服器就還是使用node.js配合Socket.IO(或是ws,如果需要透過WebSocket傳送binary資料的話),來開始結合Canvas與WebSocket。目前構想是先實作出透過WebSocket傳送資料然後透過繪圖方法來繪圖,這樣的好處是可以透過WebSocket送測試資料來做測試。之後再來解決可能存在的多人同步的問題。