之前針對白板塗鴉的需求,做好基本的抽象化。不過要利用從WebSocket傳來的資料繪圖,就需要進一步考慮。
在繪圖上,一個共享白板的技術需求主要有幾個重點:
Javascript的function是atomic的,所以只要能在一個function處理完的事情,不必擔心其他function在意料之外插入。不過在白板操作時,互動的過程會跨過好幾個事件處理函數,所以必須考慮白板狀態在這之間改變。狀態改變主要有兩個部分:
第一個要解決比較容易,我們可以在送出資料時,把需要的全域屬性一起傳送,然後在繪圖時利用save()跟restore()來恢復環境。
第二個會需要更新保存起來的繪圖前的Canvas內容,來讓繪圖完成時,寫入的資料是包含透過WebSocket傳來的動作對Canvas的異動。
有了這些構想,就可以先開始實作整合WebSocket的繪圖方法。
首先需要調整PaintTools的handle()方法,讓他接收一個callback參數,來額外處理需要處理的資訊。同時每個繪圖方法的handlers,也需要接收callback。利用這個callback,每個處理DOM事件的handler,就可以在需要的時候把資訊傳給這個callback處理。我們預計會把觸發伺服器事件的程式放在這個callback中。另外,之前在PaintTools裡面有內建的繪圖方法,不過這樣會造成繪圖方法與PaintTools耦合。我們不想調整繪圖方法時也來動PaintTools,所以把它移到PaintTools.tool.js,並且調整一下$C這個工廠方法。
不過對於drawText來說,事情就比較麻煩,因為輸入框跟綁定在輸入框上的事件都是在PaintTools呼叫各個繪圖方法的initc函數時動態產生的,這樣就沒辦法直接傳Callback給他。為了解決這個問題,除了在呼叫繪圖方法的handler時傳Callback,另外在PaintTools上設計一個registCallback方法,來透過PaintTools的instance把要使用的Callback傳送給各個繪圖方法的handler,在呼叫tool.handle(繪圖方法, event)時,如果沒有傳送Callback,就會去找是否有註冊的Callback來使用。
來看一下PaintTools的調整:
(function(window, $, undefined) {
var document = window.document;
var tools = {}, queue = [], clones = [], handlers = {}, init = [], cbs = {};
............
var factory = window['$C'] = function(ctx) {
return new PaintTools(ctx);
};
............
PaintTools.prototype.handle = function(eventType, eventObject, cb) {
if(typeof handlers[this.drawType] !== 'undefined') {
if(typeof handlers[this.drawType][eventType] !== 'undefined') {
if(typeof cb==='undefined' && this.hasCallback(this.drawType, eventType)) {
cb = this.getCallback(this.drawType, eventType);
}
handlers[this.drawType][eventType].call(this, eventObject, cb);
}
}
};
............
PaintTools.prototype.registCallback = function(drawType, handler, cb) {
if(typeof cbs[drawType] === 'undefined') cbs[drawType] = {};
cbs[drawType][handler] = cb;
return this;
};
PaintTools.prototype.getCallback = function(drawType, handler) {
if(typeof cbs[drawType] !== 'undefined') {
if(typeof cbs[drawType][handler] !== 'undefined' && typeof cbs[drawType][handler] === 'function') {
return cbs[drawType][handler];
}
}
};
PaintTools.prototype.hasCallback = function(drawType, handler) {
if(typeof cbs[drawType] !== 'undefined') {
if(typeof cbs[drawType][handler] !== 'undefined' && typeof cbs[drawType][handler] === 'function') {
return true;
}
}
}
})(window, jQuery);
雖然有點跛腳,但是還堪用。
另外,也需要設計一下透過WebSocket傳送的訊息格式。預計包含繪圖方法、資料與全域屬性三個部分。另外會需要為每種透過WebSocket傳送的繪圖動作定義一個繪圖方法,註冊給PaintTools。
為了測試剛剛寫好的registCallback機制,先拿drawText繪圖方法來測試一下:
先看一下drawText繪圖方法的調整:
var drawText = {
type: 'drawText',
run: function(text, x, y) {
this.ctx.textAlign = 'left';
this.ctx.fillText(text, x, y, this.ctx.measureText(text).width);
},
init: function() {
var self = this;
var elem = document.createElement('input');
var input = $(elem);
input.prop('type', 'text');
input.prop('id', 'keyin');
input.prop('size', '20');
input.css('display', 'none');
input.css('border', 'none');
input.css('position', 'absolute');
input.css('vertical-align', 'middle');
input.css('padding', '0 0 0 0');
input.css('margin', '0 0 0 0');
input.css('line-height', '24px');
input.bind('keypress', function(e) {
if(e.which=='13') {
self.handle('keypress', e);
//self.do('keypress', $(input).data('start')[0], $(input).data('start')[1]);
//this.drawing = false;
}
});
$(document.body).append(input);
},
handlers: {
mousedown: function(e, cb) {
if(!this.drawing) {
e.stopPropagation();
var offset = $(e.currentTarget).offset();
this.ctx.textBaseline = 'middle';
var t = $('#keyin');
t.css('left', e.pageX+1+'px');
t.css('top', e.pageY-t.outerHeight()/2+1+'px');
t.css('display', 'block');
t.focus();
this.drawing = true;
t.data('start', [e.pageX-offset.left, e.pageY-offset.top]);
}
},
mouseup: function(e, cb) {
this.drawing = false
},
keypress: function(e, cb) {
this.do($(e.currentTarget).val(), $(e.currentTarget).data('start')[0], $(e.currentTarget).data('start')[1]);
var data = $(e.currentTarget).val();
var start = $(e.currentTarget).data('start');
$(e.currentTarget).data('start', null);
this.drawing = false;
$(e.currentTarget).css('display', 'none');
$(e.currentTarget).val('');
if(typeof cb !== 'undefined' && typeof cb == 'function') {
cb({
type: 'wsDrawText',
globals: {
fillStyle: this.fillStyle,
font: this.ctx.font
},
data: {
pos: start,
text: data
}
});
}
}
}
};
tool.regist(drawText);
接下來看一下在程式中註冊Callback的的方式:
tool.registCallback('drawText', 'keypress', function(obj) {
console.log(obj);
});
這樣在操作drawText時,透過開發工具就可以看到:
接下來寫一個Socket.IO最簡單的群發來做測試,還是使用drawText。
首先,開始寫一個繪圖方法,叫做wsDrawText,然後註冊給PaintTools:
(function(window, undefined) {
// define new drawtype and method
if(window['$C']) {
var tool = window['$C'];
} else {
return;
}
///////////////////////////
var wsDrawText = {
type: 'wsDrawText',
run: function(text, x, y) {
this.ctx.textAlign = 'left';
this.ctx.fillText(text, x, y, this.ctx.measureText(text).width);
},
handlers: {
wsdraw: function(e) {
this.ctx.save();
for(var i in e.globals) {
this.ctx[i] = e.globals[i];
}
this.do(e.data.text, e.data.pos[0], e.data.pos[1]);
this.ctx.restore();
}
}
};
tool.regist(wsDrawText);
})(window);
接下來,在html中加入簡單的Socket.IO支援與程式:
............
<script src='/socket.io/socket.io.js'></script>
<script src="PaintTools.wstool.js"></script>
<script>
$(document).ready(function() {
// global variables
var socket = io.connect('ws://localhost', {
transports: ['websocket'],
"try multiple transports": false,
reconnect: true
});// configure connection settings
............
tool.registCallback('drawText', 'keypress', function(obj) {
//console.log(obj);
socket.emit('wsdraw', obj);
});
socket.on('wsdraw', function(m) {
var _old = tool.drawType;
tool.drawType = m.type;
tool.handle('wsdraw', m);
tool.drawType = _old;
});
............
最後在伺服器端加入簡單的群播訊息轉發:
............
io.sockets.on('connection', function(socket) {
socket.on('wsdraw', function(m) {
socket.broadcast.emit('wsdraw', m);
});
............
除了之前做的架構調整,要做到使用WebSocket來傳送繪圖訊息,並且在其他瀏覽器中繪圖,其實沒有寫幾行程式。
還是看一下繪圖的效果:
(由於輸入框的沒有框線,所以有白底遮住Canvas)
在另一個瀏覽器繪出後:
接下來就要把目前可支援的繪圖方法都先實作到可與WebSocket做結合,然後開始測試是否在多人協作時,會出現怎樣的問題,以及有什麼比較簡單的解決方法。(目前另外一個問題是,程式越來越複雜,光靠貼程式碼已經有點難應付...)