iT邦幫忙

DAY 19
3

且戰且走HTML5系列 第 19

且戰且走HTML5(19) 解決繪圖的多人協同運作問題

昨天發現了在多人協同繪圖時,會有無法解決的問題。問題主要在於繪圖的動作與WYSIWYG的需求不一致,導致動作的衝突無法解決。要解決這樣的衝突,方法之一就是用兩個Canvas來處理這兩種不同的需求。
其實修改PaintTools的方式還算簡單明瞭,就是讓他一次控管兩個Canvas,實際繪圖還是使用之前的Canvas,WYSIWYG效果則用另一個Canvas來做。然後調整PaintTools,首先是讓getter/setter可以把屬性同時設定到兩個Canvas,其次就是調整繪圖方法,讓不同的繪圖需求使用不同的Canvas。

首先,還是在網頁中加入一個Canvas,並且讓他疊在另一個canvas上:

 ............
 <style>
 #canvas {
 	border: solid 1px gray;
 }
 #preview {
 	border: solid 1px gray;
 	position:absolute;
 	z-index:100;
 }
 ............
 <script>
 $(document).ready(function() {
 	// global variables
 	var preview = $('#preview');
 	var canvas = $('#canvas');
 	preview.offset({left:canvas.offset().left, top:canvas.offset().top});
 ............
 	<canvas id="canvas" width='640' height='480'></canvas>
 	<canvas id="preview" width='640' height='480'></canvas>
 ............

然後來調整PaintTools,主要是在Constructor加入額外的參數,讓他可以處理另一個Canvas。另外,調整過getter/setter,讓屬性有變動時,可以同時異動到兩個canvas 2d context。另外,pushCanvas、restoreCanvas等幾個把Canvas存起來或恢復的方法,原本是對最終要畫圖的Canvas操作,現在改成對Preview用的Canvas操作。

 (function(window, $, undefined) {
 	var document = window.document;
 	var tools = {}, queue = [], clones = [], handlers = {}, init = [], cbs = {};
 	var PaintToolsProto = {
 		_drawType: 'freestyle',
 		_fontFace: 'sans-serif',
 		_fontSize: '10px',
 		_fontWeight: '400',
 		_fontStyle: 'normal',
 		get queue() {
 			return queue;
 		},
 		get fillStyle() {
 			return  this.ctx.fillStyle;
 		},
 		set fillStyle(val) {
 			this.ctx.fillStyle = val;
 			this.pre.fillStyle = val;
 			this.emit('fillStyle', val);
 		},
 		get strokeStyle() {
 			return  this.ctx.strokeStyle;
 			this.emit('fillStyle', val);
 		},
 		set strokeStyle(val) {
 			this.ctx.strokeStyle = val;
 			this.pre.strokeStyle = val;
 			this.emit('fillStyle', val);
 		},
 		get lineWidth() {
 			return this.ctx.lineWidth;
 		},
 		set lineWidth(val) {
 			this.ctx.lineWidth = val;
 			this.pre.lineWidth = val;
 			this.emit('lineWidth', val);
 		},
 		get lineCap() {
 			return  this.ctx.lineCap;
 		},
 		set lineCap(val) {
 			this.ctx.lineCap = val;
 			this.ctx.lineCap = val;
 			this.emit('lineCap', val);
 		},
 		get lineJoin() {
 			return  this.ctx.lineJoin;
 		},
 		set lineJoin(val) {
 			this.ctx.lineJoin = val;
 			this.pre.lineJoin = val;
 			this.emit('lineJoin', val);
 		},
 		get drawType() {
 			return this._drawType;
 		},
 		set drawType(val) {
 			this._drawType = val;
 			this.emit('drawType', val);
 		},
 		get fontFace() {
 			return this._fontFace;
 		},
 		set fontFace(val) {
 			this._fontFace = val;
 			this.ctx.font = this.fontStyle + ' ' + this.fontWeight + ' ' + this.fontSize + ' ' + this.fontFace;
 			this.pre.font = this.ctx.font;
 			this.emit('fontFace', val);
 			this.emit('font', this.ctx.font);
 		},
 		get fontSize() {
 			return this._fontSize;
 		},
 		set fontSize(val) {
 			this._fontSize = val;
 			this.ctx.font = this.fontStyle + ' ' + this.fontWeight + ' ' + this.fontSize + ' ' + this.fontFace;
 			this.pre.font = this.ctx.font;
 			this.emit('fontSize', val);
 			this.emit('font', this.ctx.font);
 		},
 		get fontWeight() {
 			return this._fontWeight;
 		},
 		set fontWeight(val) {
 			this._fontWeight = val;
 			this.ctx.font = this.fontStyle + ' ' + this.fontWeight + ' ' + this.fontSize + ' ' + this.fontFace;
 			this.pre.font = this.ctx.font;
 			this.emit('fontWeight', val);
 			this.emit('font', this.ctx.font);
 		},
 		get fontStyle() {
 			return this._fontStyle;
 		},
 		set fontStyle(val) {
 			this._fontStyle = val;
 			this.ctx.font = this.fontStyle + ' ' + this.fontWeight + ' ' + this.fontSize + ' ' + this.fontFace;
 			this.pre.font = this.ctx.font;
 			this.emit('fontStyle', val);
 			this.emit('font', this.ctx.font);
 		}
 	};
 	var factory = window['$C'] = function(ctx, pre) {
 		return new PaintTools(ctx, pre);
 	};
 	var PaintTools = function(_ctx, _pre) {
 		this.ctx = _ctx;
 		this.pre = _pre;
 		this.emitter = EventEmitter;
 		this.emitter();
 		delete this.emitter;
 		var self = this;
 		init.forEach(function(fn) {
 			fn.call(self);
 		});
 	};
 	PaintTools.prototype = PaintToolsProto;
 	PaintTools.prototype.regist = function(tool) {
 		if(typeof tool.type !== 'undefined') {
 			if(typeof tool.run !== 'undefined') {
 				tools[tool.type] = tool;
 			}
 			if(typeof tool.handlers !== 'undefined') {
 				handlers[tool.type] = tool.handlers;
 			}
 			if(typeof tool.init !== 'undefined') {
 				init.push(tool.init);
 			}
 		}
 	};
 	factory.regist = PaintTools.prototype.regist;
 	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.do = function() {
 		var args = [];
 		for(var i=0; i<arguments.length; i++) {
 			args.push(arguments[i]);
 		}
 		if(typeof tools[this.drawType] !== 'undefined') {
 			tools[this.drawType].run.apply(this, args);
 		}
 	};
 	PaintTools.prototype.pushCanvas = function() {
 		console.log('push canvas');
 		clones.push(this.pre.getImageData(0, 0, this.pre.canvas.width, this.pre.canvas.height));
 	};
 	PaintTools.prototype.popCanvas = function() {
 		if(clones.length>0) {
 			this.pre.putImageData(clones.pop(), 0, 0);
 		}
 	};
 	PaintTools.prototype.restoreCanvas = function() {
 		if(clones.length>0) {
 			this.pre.putImageData(clones[clones.length-1], 0, 0);
 		}
 	};
 	PaintTools.prototype.dropCanvas = function() {
 		if(clones.length>0) {
 			clones = [];
 		}
 	};
 	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);

接下來調整的是繪圖工具。這部份的調整主要是,原本的繪圖方法,內部直接呼叫了this.ctx,所以只能把圖形繪製到最終要顯示的Canvas上。為了讓他可以繪製到目標的Canvas,所以把context當做參數傳進來。另外一個部分,就是根據繪圖的目的,把圖形繪製到不同的Canvas。

 (function(window, undefined) {
 	// define new drawtype and method
 	if(window['$C']) {
 		var tool = window['$C'];
 	} else {
 		return;
 	}
 ///////////////////////////
 	var freestyle = {
 		type: 'freestyle',
 		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: {
 			mousedown: function(e, cb) {
 				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]);
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								strokeStyle: this.strokeStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [x,y,x,y]
 						});
 					}
 				}
 			},
 			mousemove: function(e, cb) {
 				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]);
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								strokeStyle: this.strokeStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			},
 			mouseup: function(e, cb) {
 				if(this.drawing) {
 					$(e.currentTarget).css('cursor', $(e.currentTarget).data('cursor'));
 					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;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								strokeStyle: this.strokeStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			},
 			mouseleave: function(e, cb) {
 				if(this.drawing) {
 					$(e.currentTarget).css('cursor', $(e.currentTarget).data('cursor'));
 					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;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								strokeStyle: this.strokeStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			}
 		}
 	}
 	tool.regist(freestyle);
 ///////////////////////////
 	var strokeRect = {
 		type: 'strokeRect',
 		run: function(ctx, x, y, x1, y1) {
 			ctx.beginPath();
 			ctx.rect(x, y, x1-x, y1-y);
 			ctx.closePath();
 			ctx.stroke();
 		},
 		handlers: {
 			mousedown: function(e, cb) {
 				if(!this.drawing) {
 					$(e.currentTarget).data('cursor', $(e.currentTarget).css('cursor'));
 					$(e.currentTarget).css('cursor', 'pointer');
 					this.drawing = true;
 					this.pushCanvas();
 					var offset = $(e.currentTarget).offset()
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(this.pre,x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e, cb) {
 				if(this.drawing) {
 					var old = this.queue.shift();
 					this.restoreCanvas();
 					this.queue.push(old);
 					var offset = $(e.currentTarget).offset();
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(this.pre,old[0],old[1],x,y);
 				}
 			},
 			mouseup: function(e, cb) {
 				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(this.ctx,old[0], old[1], x, y);
 					this.drawing = false;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								strokeStyle: this.strokeStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			},
 			mouseleave: function(e, cb) {
 				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(this.ctx,old[0], old[1], x, y);
 					this.drawing = false;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								strokeStyle: this.strokeStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			}
 		}
 	}
 	tool.regist(strokeRect);
 ///////////////////////////
 	var fillRect = {
 		type: 'fillRect',
 		run: function(ctx, x, y, x1, y1) {
 			ctx.beginPath();
 			ctx.rect(x, y, x1-x, y1-y);
 			ctx.closePath();
 			ctx.fill();
 		},
 		handlers: {
 			mousedown: function(e, cb) {
 				if(!this.drawing) {
 					this.pushCanvas();
 					$(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(this.pre,x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e, cb) {
 				if(this.drawing) {
 					var old = this.queue.shift();
 					this.restoreCanvas();
 					this.queue.push(old);
 					var offset = $(e.currentTarget).offset();
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(this.pre,old[0],old[1],x,y);
 				}
 			},
 			mouseup: function(e, cb) {
 				if(this.drawing) {
 					$(e.currentTarget).css('cursor', $(e.currentTarget).data('cursor'));
 					this.restoreCanvas();
 					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(this.ctx, old[0], old[1], x, y);
 					this.drawing = false;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								fillStyle: this.fillStyle,
 								//lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			},
 			mouseleave: function(e, cb) {
 				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(this.ctx, old[0], old[1], x, y);
 					this.drawing = false;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								fillStyle: this.fillStyle,
 								//lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			}
 		}
 	}
 	tool.regist(fillRect);
 ///////////////////////////
 	var strokeCircle = {
 		type: 'strokeCircle',
 		run: function(ctx, x1, y1, x2, y2) {
 			var radius = Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2), 2);
 			ctx.beginPath();
 			ctx.arc(x1, y1, radius, 0, 2*Math.PI, true);
 			ctx.closePath();
 			ctx.stroke();
 		},
 		handlers: {
 			mousedown: function(e, cb) {
 				if(!this.drawing) {
 					$(e.currentTarget).data('cursor', $(e.currentTarget).css('cursor'));
 					$(e.currentTarget).css('cursor', 'pointer');
 					this.drawing = true;
 					this.pushCanvas();
 					var offset = $(e.currentTarget).offset()
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(this.pre,x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e, cb) {
 				if(this.drawing) {
 					var old = this.queue.shift();
 					this.restoreCanvas();
 					this.queue.push(old);
 					var offset = $(e.currentTarget).offset();
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(this.pre,old[0],old[1],x,y);
 				}
 			},
 			mouseup: function(e, cb) {
 				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(this.ctx, old[0], old[1], x, y);
 					this.drawing = false;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								strokeStyle: this.strokeStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			},
 			mouseleave: function(e, cb) {
 				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(this.ctx, old[0], old[1], x, y);
 					this.drawing = false;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								strokeStyle: this.strokeStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			}
 		}
 	};
 	tool.regist(strokeCircle);
 ///////////////////////////
 	var fillCircle = {
 		type: 'fillCircle',
 		run: function(ctx, x1, y1, x2, y2) {
 			var radius = Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2), 2);
 			ctx.beginPath();
 			ctx.arc(x1, y1, radius, 0, 2*Math.PI, true);
 			ctx.closePath();
 			ctx.fill();
 		},
 		handlers: {
 			mousedown: function(e, cb) {
 				if(!this.drawing) {
 					$(e.currentTarget).data('cursor', $(e.currentTarget).css('cursor'));
 					$(e.currentTarget).css('cursor', 'pointer');
 					this.drawing = true;
 					this.pushCanvas();
 					var offset = $(e.currentTarget).offset()
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(this.pre,x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e, cb) {
 				if(this.drawing) {
 					var old = this.queue.shift();
 					this.restoreCanvas();
 					this.queue.push(old);
 					var offset = $(e.currentTarget).offset();
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(this.pre,old[0],old[1],x,y);
 				}
 			},
 			mouseup: function(e, cb) {
 				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(this.ctx, old[0], old[1], x, y);
 					this.drawing = false;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								fillStyle: this.fillStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			},
 			mouseleave: function(e, cb) {
 				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(this.ctx, old[0], old[1], x, y);
 					this.drawing = false;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								fillStyle: this.fillStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			}
 		}
 	};
 	tool.regist(fillCircle);
 ///////////////////////////
 	var strokeEclipse = {
 		type: 'strokeEclipse',
 		run: function(ctx, x1, y1, x2, y2) {
 			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);
 			ctx.moveTo(x1, y1);
 			ctx.bezierCurveTo(xc1, yc1, xc2, yc2, xx2, yy2);
 			ctx.bezierCurveTo(xc3, yc3, xc4, yc4, x1, y1);
 			ctx.closePath();
 			ctx.stroke();
 		},
 		handlers: {
 			mousedown: function(e, cb) {
 				if(!this.drawing) {
 					$(e.currentTarget).data('cursor', $(e.currentTarget).css('cursor'));
 					$(e.currentTarget).css('cursor', 'pointer');
 					this.drawing = true;
 					this.pushCanvas();
 					var offset = $(e.currentTarget).offset()
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(this.pre,x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e, cb) {
 				if(this.drawing) {
 					var old = this.queue.shift();
 					this.restoreCanvas();
 					this.queue.push(old);
 					var offset = $(e.currentTarget).offset();
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(this.pre,old[0],old[1],x,y);
 				}
 			},
 			mouseup: function(e, cb) {
 				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(this.ctx, old[0], old[1], x, y);
 					this.drawing = false;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								strokeStyle: this.strokeStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			},
 			mouseleave: function(e, cb) {
 				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(this.ctx, old[0], old[1], x, y);
 					this.drawing = false;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								strokeStyle: this.strokeStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			}
 		}
 	};
 	tool.regist(strokeEclipse);
 ///////////////////////////
 	var fillEclipse = {
 		type: 'fillEclipse',
 		run: function(ctx, x1, y1, x2, y2) {
 			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);
 			ctx.moveTo(x1, y1);
 			ctx.bezierCurveTo(xc1, yc1, xc2, yc2, xx2, yy2);
 			ctx.bezierCurveTo(xc3, yc3, xc4, yc4, x1, y1);
 			ctx.closePath();
 			ctx.fill();
 		},
 		handlers: {
 			mousedown: function(e, cb) {
 				if(!this.drawing) {
 					$(e.currentTarget).data('cursor', $(e.currentTarget).css('cursor'));
 					$(e.currentTarget).css('cursor', 'pointer');
 					this.drawing = true;
 					this.pushCanvas();
 					var offset = $(e.currentTarget).offset()
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(this.pre,x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e, cb) {
 				if(this.drawing) {
 					var old = this.queue.shift();
 					this.restoreCanvas();
 					this.queue.push(old);
 					var offset = $(e.currentTarget).offset();
 					var x = e.pageX - offset.left;
 					var y = e.pageY - offset.top;
 					this.do(this.pre,old[0],old[1],x,y);
 				}
 			},
 			mouseup: function(e, cb) {
 				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(this.ctx, old[0], old[1], x, y);
 					this.drawing = false;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								fillStyle: this.fillStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			},
 			mouseleave: function(e, cb) {
 				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(this.ctx, old[0], old[1], x, y);
 					this.drawing = false;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							globals: {
 								fillStyle: this.fillStyle,
 								lineWidth: this.lineWidth
 							},
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			}
 		}
 	};
 	tool.regist(fillEclipse);
 ///////////////////////////
 	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.css('z-index', '101');
 			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: this.drawType,
 						globals: {
 							fillStyle: this.fillStyle,
 							font: this.ctx.font
 						},
 						data: {
 							pos: start,
 							text: data
 						}
 					});
 				}
 			}
 		}
 	};
 	tool.regist(drawText);
 ///////////////////////////
 	var eraser = {
 		type: 'eraser',
 		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: {
 			mousedown: function(e, cb) {
 				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]);
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							data: [x,y,x,y]
 						});
 					}
 				}
 			},
 			mousemove: function(e, cb) {
 				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]);
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			},
 			mouseup: function(e, cb) {
 				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;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			},
 			mouseleave: function(e, cb) {
 				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;
 					if(typeof cb !== 'undefined' && typeof cb == 'function') {
 						cb({
 							type: this.drawType,
 							data: [old[0],old[1],x,y]
 						});
 					}
 				}
 			}
 		}
 	}
 	tool.regist(eraser);
 })(window);

最後,在網頁中實體化PaintTools時,只要把兩個Canvas 2D Context傳給他,而原先綁定在最終畫面用的Canvas上的事件,改綁定到Preview用的Canvas上。兩個Canvas的座標是一致的,所以收到的Event物件座標可以直接對另一個Canvas使用。

 ............
 	var context = canvas[0].getContext('2d');
 	var context1 = preview[0].getContext('2d');
 	var tool = $C(context, context1);
 ............
 	//drawing dom event
 	preview.bind('mousedown', function(e) {
 		e.preventDefault();
 		tool.handle('mousedown', e, wsCallBack);
 	});
 	preview.bind('mousemove', function(e) {
 		e.preventDefault();
 		tool.handle('mousemove', e, wsCallBack);
 	});
 	preview.bind('mouseup', function(e) {
 		tool.handle('mouseup', e, wsCallBack);
 	});
 	preview.bind('mouseleave', function(e) {
 		tool.handle('mouseleave', e, wsCallBack);
 	});
 ............

處理完後,就可以在Canvas中任意繪圖,不會跟從WebSocket觸發的繪圖互相干擾了。不過這樣就真正同步了嗎?其實未必。不過塗鴉應用跟編輯文章不太一樣,因為不會有「衝突」(也就是兩人改到同一個內容),只會有順序的問題。要解決這個問題,需要利用WebSocket伺服器。也就是說,最後呈現的畫面,需要嚴格依照從WebSocket收到資料的順序。之前本地端的動作,是在最終繪出的畫面決定後,就立刻畫到Canvas上。現在這個動作需要改成,先把資料送出到WebSocket伺服器,然後WebSocket伺服器再群發,然後要到收到從WebSocket伺服器的資料,才真正把圖畫到Canvas。Socket.IO的broadcast,會排除自己,如果是用Socket.IO的話,就需要調整訊息發送的對象。

不過已經在Canvas這個主題停太久了(這樣已經超過原先的計畫),這部份就不先實作了。明天開始先來看看File API,同時也開始嘗試是否用WebSocket可以建立直接的檔案分享,而不需經過上傳實體檔案、建立連結等的過程。預計不會在FileAPI停很久,很快就會進入WebRTC。希望在最後幾天,可以把這些應用整合起來,成為一個完整的應用。


上一篇
且戰且走HTML5(18) 再看多人協同運作
下一篇
且戰且走HTML5(20) 資源共享
系列文
且戰且走HTML530
0
fillano
iT邦超人 1 級 ‧ 2012-10-27 01:08:03

接下來繼續趕簡報XD

我要留言

立即登入留言