iT邦幫忙

DAY 17
4

且戰且走HTML5系列 第 17

且戰且走HTML5(17) 將Canvas繪圖進一步抽象

之前針對白板塗鴉的需求,做好基本的抽象化。不過要利用從WebSocket傳來的資料繪圖,就需要進一步考慮。
在繪圖上,一個共享白板的技術需求主要有幾個重點:

  1. 在本地繪圖時,收集繪圖資訊,然過WebSocket算送給其他共享白板的使用者
  2. 接收從WebSocket收到的繪圖資訊,根據這個資訊在白板上繪圖
  3. 同時,還要能正常操作白板

Javascript的function是atomic的,所以只要能在一個function處理完的事情,不必擔心其他function在意料之外插入。不過在白板操作時,互動的過程會跨過好幾個事件處理函數,所以必須考慮白板狀態在這之間改變。狀態改變主要有兩個部分:

  1. 2D Context的全域屬性修改
  2. Canvas被畫出新的圖案,這會影響圖形繪製的功能,因為在沒有拉好圖形前,需要記錄Canvas在開始繪圖動作前的內容,然後在滑鼠移動時重新繪製內容與圖形。

第一個要解決比較容易,我們可以在送出資料時,把需要的全域屬性一起傳送,然後在繪圖時利用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做結合,然後開始測試是否在多人協作時,會出現怎樣的問題,以及有什麼比較簡單的解決方法。(目前另外一個問題是,程式越來越複雜,光靠貼程式碼已經有點難應付XD...)


上一篇
且戰且走HTML5(16) Canvas與Websocket整合
下一篇
且戰且走HTML5(18) 再看多人協同運作
系列文
且戰且走HTML530

2 則留言

0
fillano
iT邦超人 1 級 ‧ 2012-10-25 00:17:24

看來貼程式碼有點問題(剛剛也看到有人在反映),請略過程式碼中的br。

fillano iT邦超人 1 級‧ 2012-10-25 14:49:27 檢舉

解決了,感謝小財神。

0
ted99tw
iT邦高手 1 級 ‧ 2012-10-25 11:11:06

有br真的粉影響閱讀啊....Orz

我要留言

立即登入留言