iT邦幫忙

DAY 15
4

且戰且走HTML5系列 第 15

且戰且走HTML5(15) Canvas基本繪圖-架構調整

之前的程式碼看起來有些凌亂,所以稍微停下腳步,整理一下。
其實本來其實想自己實作AMD(Asynchronous Module Define),不過時間來不及,所以還是針對程式本身做一些調整就好XD

首先就是把一些獨立的功能拉出去,包括EventEmitter、PaintTools以及繪圖方法等。不過這時候會發現一個問題,就要把繪圖方法註冊到PaintTools時,原來使用的是PaintTools的instance,但是如果這樣做,這些外部的繪圖方法就不容易拆出去獨立的Javascript檔案了。由於這部份是使用Closure來實作,所以不論利用PaintTools的實體或是$C這個工廠方法,其實都做得到。所以加上一行程式:

 	var factory = window['$C'] = function(ctx) {
 		var ret = new PaintTools(ctx);
 		ret.regist(freestyle);
 		ret.regist(strokeRect);
 		ret.regist(fillRect);
 		return ret;
 	};
 	var PaintTools = function(_ctx) {
 		this.ctx = _ctx;
 		this.emitter = EventEmitter;
 		this.emitter();
 		delete this.emitter;
 	};
 	PaintTools.prototype = PaintToolsProto;
 	PaintTools.prototype.regist = function(tool) {
 		if(typeof tool.type !== 'undefined' && typeof tool.run !== 'undefined') {
 			tools[tool.type] = tool;
 		}
 	};
 	factory.regist = PaintTools.prototype.regist;

由於regist除了使用tools變數之外,沒有其他依賴性,把它再assign給factory(也就是$C),執行起來跟他作為PaintTools的instance的方法是沒差別的。

接下來就可以把一些外部繪圖工具也拆到獨立的js檔案:

 (function(window, undefined) {
 	// define new drawtype and method
 	if(window['$C']) {
 		var tool = window['$C'];
 	} else {
 		return;
 	}
 	var strokeCircle = {
 		type: 'strokeCircle',
 		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();
 		}
 	};
 	tool.regist(strokeCircle);
 	var fillCircle = {
 		type: 'fillCircle',
 		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();
 		}
 	};
 	tool.regist(fillCircle);
 	var strokeEclipse = {
 		type: 'strokeEclipse',
 		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();
 		}
 	}
 	tool.regist(strokeEclipse);
 	var fillEclipse = {
 		type: 'fillEclipse',
 		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();
 		}
 	}
 	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);
 		}
 	}
 	tool.regist(drawText);
 	
 })(window);

這樣html裡面的程式行數大概就減少了一半。接著看一下處理繪圖的DOM事件處理函數,這樣會發現一個問題,就是很多種繪圖方法都共用一個事件,直覺之下就先使用if做判斷。但是這樣寫程式,每加一個繪圖方法,恐怕就需要去if增加一個條件,而在不同繪圖方法間可能會需要額外處理,這樣就又需要額外的if做判斷,程式很難維護。

其實對PaintTools來說,同時只會使用一種繪圖方法,這個繪圖方法的狀態,也是PaintTools自己在維護的。既然是這樣,乾脆不用外部的if來判斷繪圖狀態,直接把事件處理函數要做得事情也交給繪圖方法來管理就可以了。為了達到這個目的,需要給PaintTools加入新的方法,而個別繪圖方法物件,也要額外加上處理DOM事件的函數,在註冊時交由PaintTools來管理。在DOM事件處理函數中,則呼叫新增的方法,把事件名稱作為第一個參數,事件物件作為第二個參數給他處理。

對於文字繪製的繪圖方法,還需要額外的加工,因為他需要在網頁中嵌入text input元素。為此,在regist中額外加入處理繪圖方法的init函數,在PaintTools的constructor呼叫這些init函數。

把事件處理函數也移出後,主體的html看起來清爽多了:

 <meta charset='utf-8'>
 
 <link rel="StyleSheet" type="text/css" href="reset.css">
 <style>
 canvas {
 	border: solid 1px gray;
 }
 .panel {
 	margin: 5px 5px 5px 5px;
 	padding: 5px 5px 5px 5px;
 	border: solid 2px #336699;
 	width: 778px;
 }
 .tools {
 	margin-right: 5px;
 	padding: 2px 2px 2px 2px;
 	border: solid 2px #6699CC;
 	width: 120px;
 	height: 474px;
 	float:left;
 }
 label {
 	font-size: 10px;
 }
 span {
 	font-size: 9px;
 	color: red;
 	font-weight: bold;
 }
 </style>
 <link rel="stylesheet" href="js/colorPicker.css" type="text/css" />
 <script src='http://code.jquery.com/jquery-1.8.2.js'></script>
 <script src='js/jquery.colorPicker.js'></script>
 <script src='EventEmitter.js'></script>
 <script src='PaintTools.js'></script>
 <script src='PaintTools.tools.js'></script>
 <script>
 $(document).ready(function() {
 	// global variables
 	var canvas = $('#canvas');
 	var context = canvas[0].getContext('2d');
 	var tool = $C(context);
 
 	// drawing tool setup
 	tool.on('drawType', function(v) {$('#drawtypestatus').html(v);});
 	tool.lineCap = 'round';
 	tool.lineJoin = 'round';
 	tool.lineWidth = 1;
 
 	// color picker setup
 	$('#color1').colorPicker({
 		pickerDefault: 'ffffff',
 		onColorChange: function(id, val) {
 			tool.fillStyle = val;
 		}
 	});
 	$('#color2').colorPicker({
 		pickerDefault: 'ffffff',
 		onColorChange: function(id, val) {
 			tool.strokeStyle = val;
 		}
 	});
 
 	// change fillStyle and font of PaintTools will also change those of text input
 	tool.on('fillStyle', function(val) {
 		$('#keyin').css('color', val);
 	});
 	tool.on('font', function(val) {
 		$('#keyin').css('font', val);
 	});
 
 	//drawing dom event
 	canvas.bind('mousedown', function(e) {
 		e.preventDefault();
 		tool.handle('mousedown', e);
 	});
 	canvas.bind('mousemove', function(e) {
 		e.preventDefault();
 		tool.handle('mousemove', e);
 	});
 	canvas.bind('mouseup', function(e) {
 		tool.handle('mouseup', e);
 	});
 	canvas.bind('mouseleave', function(e) {
 		tool.handle('mouseleave', e);
 	});
 
 	// tool panel event
 	$('#linewidth').bind('change', function() {
 		tool.lineWidth = $(this).val();
 	});
 	$('#freestyle').bind('click', function() {
 		tool.drawType = 'freestyle';
 	});
 	$('#strokerect').bind('click', function() {
 		tool.drawType = 'strokeRect';
 	});
 	$('#fillrect').bind('click', function() {
 		tool.drawType = 'fillRect';
 	});
 	$('#strokecircle').bind('click', function() {
 		tool.drawType = 'strokeCircle';
 	});
 	$('#fillcircle').bind('click', function() {
 		tool.drawType = 'fillCircle';
 	});
 	$('#strokeeclipse').bind('click', function() {
 		tool.drawType = 'strokeEclipse';
 	});
 	$('#filleclipse').bind('click', function() {
 		tool.drawType = 'fillEclipse';
 	});
 	$('#drawtext').bind('click', function() {
 		tool.drawType = 'drawText';
 	});
 	$('#fontface').bind('change', function() {
 		tool.fontFace = $(this).val();
 	});
 	$('#fontsize').bind('change', function() {
 		tool.fontSize = $(this).val();
 	});
 	$('#fontweight').bind('change', function() {
 		tool.fontWeight = $(this).val();
 	});
 	$('#fontstyle').bind('change', function() {
 		tool.fontStyle = $(this).val();
 	});
 
 });
 </script>
 
 
 <div class='panel'>
 	<div class='tools' id='tools'>
 		<div>
 			<label for='drawtypestatus'>Draw Type: </label><span id='drawtypestatus'></span><br>
 			<hr size='1' width='100%'>
 		</div>
 		<div>
 			<label for='color1'>Fill: </label><div style='display:inline-block'><input type='text' id='color1' name='color1' value='#000000'></div><br>
 			<label for='color2'>Stroke: </label><div style='display:inline-block'><input type='text' id='color2' name='color2' value='#000000'></div><br>
 			<label for='linewidth'>Line Width: </label><select id='linewidth'>
 				<option value="1">1</option>
 				<option value="3">3</option>
 				<option value="5">5</option>
 				<option value="7">7</option>
 			</select>
 			<hr size='1' width='100%'>
 		</div>
 		<div>
 			<button id="freestyle">freesytle</button>
 			<button id='strokerect'>Stroke Rectangle</button>
 			<button id='fillrect'>Fill Rectangle</button>
 			<button id='strokecircle'>Stroke Circle</button>
 			<button id='fillcircle'>Fill Circle</button>
 			<button id='strokeeclipse'>Stroke Eclipse</button>
 			<button id='filleclipse'>Fill Eclipse</button>
 			<button id='drawtext'>Draw Text</button>
 			<hr size='1' width='100%'>
 		</div>
 		<div>
 			<label for='fontface'>Font: </label><select id='fontface'>
 				<option value='sans-serif'>sans-serif</option>
 				<option value='serif'>serif</option>
 				<option value='cursive'>cursive</option>
 				<option value='fantasy'>fantasy</option>
 				<option value='monospace'>monospace</option>
 			</select>
 			<label>Size: </label><select id='fontsize'>
 				<option value='10px'>10px</option>
 				<option value='12px'>12px</option>
 				<option value='14px'>14px</option>
 				<option value='16px'>16px</option>
 				<option value='18px'>18px</option>
 				<option value='20px'>20px</option>
 				<option value='24px'>24px</option>
 			</select>
 			<label>Weight: </label><select id='fontweight'>
 				<option value='400'>default</option>
 				<option value='700'>bold</option>
 			</select>
 			<label>Style: </label><select id='fontstyle'>
 				<option value='normal'>normal</option>
 				<option value='italic'>italic</option>
 			</select>
 		</div>
 	</div>
 	<canvas id="canvas" width='640' height='480'></canvas>
 </div>
 
 

不過這樣繪圖工具會有不少重複的程式碼,程式會比較長。先來看PaintTools:

 (function(window, $, undefined) {
 	var document = window.document;
 	var tools = {}, queue = [], clones = [], handlers = {}, init = [];
 	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.emit('fillStyle', val);
 		},
 		get strokeStyle() {
 			return  this.ctx.strokeStyle;
 			this.emit('fillStyle', val);
 		},
 		set strokeStyle(val) {
 			this.ctx.strokeStyle = val;
 			this.emit('fillStyle', val);
 		},
 		get lineWidth() {
 			return this.ctx.lineWidth;
 		},
 		set lineWidth(val) {
 			this.ctx.lineWidth = val;
 			this.emit('lineWidth', val);
 		},
 		get lineCap() {
 			return  this.ctx.lineCap;
 		},
 		set lineCap(val) {
 			this.ctx.lineCap = val;
 			this.emit('lineCap', val);
 		},
 		get lineJoin() {
 			return  this.ctx.lineJoin;
 		},
 		set lineJoin(val) {
 			this.ctx.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.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.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.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.emit('fontStyle', val);
 			this.emit('font', this.ctx.font);
 		}
 	};
 	var factory = window['$C'] = function(ctx) {
 		var ret = new PaintTools(ctx);
 		ret.regist(freestyle);
 		ret.regist(strokeRect);
 		ret.regist(fillRect);
 		return ret;
 	};
 	var PaintTools = function(_ctx) {
 		this.ctx = _ctx;
 		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) {
 		if(typeof handlers[this.drawType] !== 'undefined') {
 			if(typeof handlers[this.drawType][eventType] !== 'undefined') {
 				handlers[this.drawType][eventType].call(this, eventObject);
 			}
 		}
 	};
 	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() {
 		clones.push(this.ctx.getImageData(0, 0, this.ctx.canvas.width, this.ctx.canvas.height));
 	};
 	PaintTools.prototype.popCanvas = function() {
 		if(clones.length>0) {
 			this.ctx.putImageData(clones.pop(), 0, 0);
 		}
 	};
 	PaintTools.prototype.restoreCanvas = function() {
 		if(clones.length>0) {
 			this.ctx.putImageData(clones[0], 0, 0);
 		}
 	};
 	PaintTools.prototype.dropCanvas = function() {
 		if(clones.length>0) {
 			clones.pop();
 		}
 	}
 	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) {
 				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;
 				}
 			}
 		}
 	}
 	var strokeRect = {
 		type: 'strokeRect',
 		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: {
 			mousedown: function(e) {
 				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(x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e) {
 				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(old[0],old[1],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;
 				}
 			}
 		}
 	}
 	var fillRect = {
 		type: 'fillRect',
 		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: {
 			mousedown: function(e) {
 				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(x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e) {
 				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(old[0],old[1],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;
 				}
 			}
 		}
 	}
 
 })(window, jQuery);

沒有內建在PaintTools的繪圖工具:

 (function(window, undefined) {
 	// define new drawtype and method
 	if(window['$C']) {
 		var tool = window['$C'];
 	} else {
 		return;
 	}
 	var strokeCircle = {
 		type: 'strokeCircle',
 		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: {
 			mousedown: function(e) {
 				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(x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e) {
 				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(old[0],old[1],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(strokeCircle);
 	var fillCircle = {
 		type: 'fillCircle',
 		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: {
 			mousedown: function(e) {
 				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(x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e) {
 				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(old[0],old[1],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(fillCircle);
 	var strokeEclipse = {
 		type: 'strokeEclipse',
 		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: {
 			mousedown: function(e) {
 				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(x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e) {
 				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(old[0],old[1],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(strokeEclipse);
 	var fillEclipse = {
 		type: 'fillEclipse',
 		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: {
 			mousedown: function(e) {
 				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(x,y,x,y);
 					this.queue.push([x,y]);
 				}
 			},
 			mousemove: function(e) {
 				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(old[0],old[1],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(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.bind('keypress', function(e) {
 				if(e.which=='13') {
 					self.do($(this).val(), $(this).data('start')[0], $(this).data('start')[1]);
 					$(this).data('start', null);
 					self.drawing = false;
 					$(this).css('display', 'none');
 					$(this).val('');
 				}
 			});
 			$(document.body).append(input);
 		},
 		handlers: {
 			mousedown: function(e) {
 				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) {
 				this.drawing = false
 			}
 		}
 	};
 	tool.regist(drawText);
 	
 })(window);

稍微測試一下,跑起來跟之前一樣。

其實還少一點功能,不過因為這些比較簡單,我就不特別實作,有空再加到程式中。首先是橡皮擦,其實橡皮擦只是一個特製的塗鴉功能,他的線條寬度較寬而且是固定的,而填色使用RGBA(0,0,0,0)。這樣線條畫過的地方,就會把Canvas畫面清掉。如果要把整個Canvas畫面清空就更簡單了,只要呼叫2D Context的clearRect(x, y, w, h)就可以,呼叫時把x, y都設為0,然後w, h設為Canvas的寬跟高,就可以清除掉Canvas可見區域的畫面。

這系列都沒用到旋轉、放大縮小、位移、變形等功能,不過要實作這些功能,會需要更繁複的操作步驟,畫面的控制也更麻煩,就不去實作了。(直接呼叫是很簡單,但是要做出能與使用者互動的繪圖效果很麻煩)

OK,就先把Canvas告一段落,明天開始整合Canvas跟WebSocket,來做出多人共享白板。


上一篇
且戰且走HTML5(14) Canvas基本繪圖-打字
下一篇
且戰且走HTML5(16) Canvas與Websocket整合
系列文
且戰且走HTML530
0
ted99tw
iT邦高手 1 級 ‧ 2012-10-23 08:54:28

讚讚讚

“且戰且走”都還這麼猛.....囧

0
fillano
iT邦超人 1 級 ‧ 2012-10-23 11:32:46

補充一下,這裡消除if判斷的方法,好像是應用strategy pattern。(我大概已經把pattern忘光了)其實應該拆成兩個類別的,這樣給PaintTools的職責太多了,只是我太懶...所以他就變成了神之類別XD

我要留言

立即登入留言