iT邦幫忙

DAY 18
5

且戰且走HTML5系列 第 18

且戰且走HTML5(18) 再看多人協同運作

單人操作白板,或是在瀏覽器間交錯操作,對運作的影響都不太大,但是我們要做的是多人同時操作,這時會發生什麼事呢?
昨天其實只試做出一個透過WebSocket協同運作的繪圖方法,在測試多人協同的問題前,要先把所有的繪圖方法完成。

 	function wsCallBack(obj) {
 		switch(obj.type) {
 			case 'drawText':
 				obj.type = 'wsDrawText';
 				break;
 			case 'freestyle':
 				obj.type = 'wsDrawLine';
 				break;
 			case 'strokeRect':
 				obj.type = 'wsStrokeRect';
 				break;
 			case 'fillRect':
 				obj.type = 'wsFillRect';
 				break;
 			case 'strokeCircle':
 				obj.type = 'wsStrokeCircle';
 				break;
 			case 'fillCircle':
 				obj.type = 'wsFillCircle';
 				break;
 			case 'strokeEclipse':
 				obj.type = 'wsStrokeEclipse';
 				break;
 			case 'fillEclipse':
 				obj.type = 'wsFillEclipse';
 				break;
 			case 'eraser':
 				obj.type = 'wsEraser';
 				break;
 		}
 		socket.emit('wsdraw', obj);
 	}

首先,就是調整一下在drawText使用的Callback,讓他對所有的繪圖方法都有效。由於在繪圖方法中傳給callback是自己的名字,在這裡把它改成實際會透過WebSocket觸發的繪圖方法的名字。

另外,就是把所有透過WebSocket繪圖的方法都實作出來:

 (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);
 ///////////////////////////
 	var wsDrawLine = {
 		type: 'wsDrawLine',
 		run: function(x, y, x1, y1) {
 			this.ctx.beginPath();
 			this.ctx.moveTo(x,y);
 			this.ctx.lineTo(x1,y1);
 			this.ctx.closePath();
 			this.ctx.stroke();
 		},
 		handlers: {
 			wsdraw: function(e) {
 				this.ctx.save();
 				for(var i in e.globals) {
 					this.ctx[i] = e.globals[i];
 				}
 				this.do(e.data[0], e.data[1], e.data[2], e.data[3]);
 				this.ctx.restore();
 			}
 		}
 	};
 	tool.regist(wsDrawLine);
 ///////////////////////////
 	var wsStrokeRect = {
 		type: 'wsStrokeRect',
 		run: function(x, y, x1, y1) {
 			this.ctx.beginPath();
 			this.ctx.rect(x, y, x1-x, y1-y);
 			this.ctx.closePath();
 			this.ctx.stroke();
 		},
 		handlers: {
 			wsdraw: function(e) {
 				this.ctx.save();
 				for(var i in e.globals) {
 					this.ctx[i] = e.globals[i];
 				}
 				this.do(e.data[0], e.data[1], e.data[2], e.data[3]);
 				this.ctx.restore();
 			}
 		}
 	};
 	tool.regist(wsStrokeRect);
 ///////////////////////////
 	var wsFillRect = {
 		type: 'wsFillRect',
 		run: function(x, y, x1, y1) {
 			this.ctx.beginPath();
 			this.ctx.rect(x, y, x1-x, y1-y);
 			this.ctx.closePath();
 			this.ctx.fill();
 		},
 		handlers: {
 			wsdraw: function(e) {
 				this.ctx.save();
 				for(var i in e.globals) {
 					this.ctx[i] = e.globals[i];
 				}
 				this.do(e.data[0], e.data[1], e.data[2], e.data[3]);
 				this.ctx.restore();
 			}
 		}
 	};
 	tool.regist(wsFillRect);
 ///////////////////////////
 	var wsStrokeCircle = {
 		type: 'wsStrokeCircle',
 		run: function(x1, y1, x2, y2) {
 			var radius = Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2), 2);
 			this.ctx.beginPath();
 			this.ctx.arc(x1, y1, radius, 0, 2*Math.PI, true);
 			this.ctx.closePath();
 			this.ctx.stroke();
 		},
 		handlers: {
 			wsdraw: function(e) {
 				this.ctx.save();
 				for(var i in e.globals) {
 					this.ctx[i] = e.globals[i];
 				}
 				this.do(e.data[0], e.data[1], e.data[2], e.data[3]);
 				this.ctx.restore();
 			}
 		}
 	};
 	tool.regist(wsStrokeCircle);
 ///////////////////////////
 	var wsFillCircle = {
 		type: 'wsFillCircle',
 		run: function(x1, y1, x2, y2) {
 			var radius = Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2), 2);
 			this.ctx.beginPath();
 			this.ctx.arc(x1, y1, radius, 0, 2*Math.PI, true);
 			this.ctx.closePath();
 			this.ctx.fill();
 		},
 		handlers: {
 			wsdraw: function(e) {
 				this.ctx.save();
 				for(var i in e.globals) {
 					this.ctx[i] = e.globals[i];
 				}
 				this.do(e.data[0], e.data[1], e.data[2], e.data[3]);
 				this.ctx.restore();
 			}
 		}
 	};
 	tool.regist(wsFillCircle);
 ///////////////////////////
 	var wsStrokeEclipse = {
 		type: 'wsStrokeEclipse',
 		run: function(x1, y1, x2, y2) {
 			this.ctx.beginPath();
 			var xc1 = x1, yc1 = y1 - Math.abs(y2-y1), xc2 = x2, yc2 = y1 - Math.abs(y2-y1), xx2 = x2, yy2 = y1, 
 				xc3 = x2, yc3 = y1 + Math.abs(y2-y1), xc4 = x1, yc4 = y1 + Math.abs(y2-y1);
 			this.ctx.moveTo(x1, y1);
 			this.ctx.bezierCurveTo(xc1, yc1, xc2, yc2, xx2, yy2);
 			this.ctx.bezierCurveTo(xc3, yc3, xc4, yc4, x1, y1);
 			this.ctx.closePath();
 			this.ctx.stroke();
 		},
 		handlers: {
 			wsdraw: function(e) {
 				this.ctx.save();
 				for(var i in e.globals) {
 					this.ctx[i] = e.globals[i];
 				}
 				this.do(e.data[0], e.data[1], e.data[2], e.data[3]);
 				this.ctx.restore();
 			}
 		}
 	};
 	tool.regist(wsStrokeEclipse);
 ///////////////////////////
 	var wsFillEclipse = {
 		type: 'wsFillEclipse',
 		run: function(x1, y1, x2, y2) {
 			this.ctx.beginPath();
 			var xc1 = x1, yc1 = y1 - Math.abs(y2-y1), xc2 = x2, yc2 = y1 - Math.abs(y2-y1), xx2 = x2, yy2 = y1, 
 				xc3 = x2, yc3 = y1 + Math.abs(y2-y1), xc4 = x1, yc4 = y1 + Math.abs(y2-y1);
 			this.ctx.moveTo(x1, y1);
 			this.ctx.bezierCurveTo(xc1, yc1, xc2, yc2, xx2, yy2);
 			this.ctx.bezierCurveTo(xc3, yc3, xc4, yc4, x1, y1);
 			this.ctx.closePath();
 			this.ctx.fill();
 		},
 		handlers: {
 			wsdraw: function(e) {
 				this.ctx.save();
 				for(var i in e.globals) {
 					this.ctx[i] = e.globals[i];
 				}
 				this.do(e.data[0], e.data[1], e.data[2], e.data[3]);
 				this.ctx.restore();
 			}
 		}
 	};
 	tool.regist(wsFillEclipse);
 ///////////////////////////
 	var wsEraser = {
 		type: 'wsEraser',
 		run: function(x, y, x1, y1) {
 			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: {
 			wsdraw: function(e) {
 				this.ctx.save();
 				for(var i in e.globals) {
 					this.ctx[i] = e.globals[i];
 				}
 				this.do(e.data[0], e.data[1], e.data[2], e.data[3]);
 				this.ctx.restore();
 			}
 		}
 	};
 	tool.regist(wsEraser);
 })(window);

這樣,就可以透過WebSocket觸發所有的繪圖動作。

接下來就要測試了,測試方法很簡單,就是在Socket.IO伺服器中加一段程式,每隔一秒畫一條隨機的直線:

 	setInterval(function() {
 		var x1 = Math.round(Math.random()*1000)%640;
 		var y1 = Math.round(Math.random()*1000)%480;
 		var x2 = Math.round(Math.random()*1000)%640;
 		var y2 = Math.round(Math.random()*1000)%480;
 		socket.emit('wsdraw', {type:'wsDrawLine', globals:{strokeStyle: '#336699',lineWidth:1}, data:[x1,y1,x2,y2]});
 	}, 1000);

然後在WebSocket觸發繪圖的同時,操作白板。

雖然從靜態的抓圖中看不太出來,但是在做拉出圖形的效果時,是先把canvas存起來,然後拉圖形的過程中,先把存起來的canvas寫回,再畫出拉到目前座標的圖形。但是這樣一來,在拉圖形的過程中,WebSocket觸發的繪圖就不斷消失XD

先嘗試一個解法,是讓WebSocket觸發的繪圖最後,更新存起來的canvas。這樣看起來好像比較好,至少WebSocket觸發的繪圖不會消失,但是這樣又發生另外一個問題...我們拉的圖形只是為了看到圖形畫成怎樣,直到mouseup或mouseleave,畫出的才是要真正畫出的圖形。但是...由於WebSocket觸發的繪圖更新存起來的Canvas,就影響到這個過程,結果中途拉的這些圖形就被畫到白板上而不會消失,這並不是我們要的結果。

既然是這樣,如果在WebSocket觸發的繪圖之初,先恢復(清掉拉圖形過程中出現的繪圖)Canvas,這樣就不會把這些過程中的示意用的繪圖寫入,但是這樣拉的圖形如果暫時沒拖動,就會消失(拖動的話又會出現)。

要讓示意用的繪圖畫在最終要繪出的Canvas上,就會造成這樣的衝突,而且單靠這樣簡單的儲存、寫回Canvas的動作,是沒辦法解決的。看來唯一的解法,是用兩個Canvas來實作。疊在上方的Canvas,負責顯示繪圖過程中出現的示意用圖形,到了決定(mouseup或mouseleave)時,才清掉臨時的Canvas把圖形畫到最終顯示的Canvas,這樣兩種繪圖的動作才不會衝突。

這樣又需要大幅改動PaintTools的架構了,等明天再來做吧XD


上一篇
且戰且走HTML5(17) 將Canvas繪圖進一步抽象
下一篇
且戰且走HTML5(19) 解決繪圖的多人協同運作問題
系列文
且戰且走HTML530

2 則留言

0
SunAllen
iT邦高手 1 級 ‧ 2012-10-26 00:38:20

費大的文章,真的很酷!讚讚讚

0
ted99tw
iT邦高手 1 級 ‧ 2012-10-26 07:47:23

真的很酷,費大的文章!灑花灑花灑花

我要留言

立即登入留言