iT邦幫忙

DAY 11
2

且戰且走HTML5系列 第 11

且戰且走HTML5(11) Canvas基本繪圖-塗鴉

要製作一個簡單的繪圖應用,自由塗鴉應該是最基本的功能之一。所以就先來看一下怎麼做出來。
塗鴉其實只使用到2D Context的幾個操作方法,主要是lineTo與滑鼠事件的搭配。lineTo是路徑的操作,要構成路徑,除了lineTo,還需要與moveTo、beginPath、closePath等方法做搭配。結束後,才用stroke來真正繪製出線條。

整個過程大概是像這樣:

  1. 按下滑鼠左鍵,觸發mousedown事件:開始繪圖,需要把事件發生的座標先記下來。
  2. 移動滑鼠,觸發mousemove事件;每次觸發時,就從前一次記錄的座標,呼叫lineTo畫直線到目前的座標,並且把目前座標也存起來,給下一次lineTo使用
  3. 放開滑鼠左鍵,觸發mouseup事件:一樣lineTo到事件觸發的座標,然後結束繪圖

mousemove事件觸發其實並不會構成一個連續不斷的路徑,所以必須利用這種方式才能做出塗鴉的效果。

先來看一個簡單的示範:

先寫一個簡單的繪製直線函數:

 	function drawLine(ctx, x, y, x1, y1) {
 		ctx.beginPath();
 		ctx.moveTo(x,y);
 		ctx.lineTo(x1,y1);
 		ctx.closePath();
 		ctx.stroke();
 	}

接者處理在Canvas上觸發的事件,不過要先注意,塗鴉的動作其實跟「選擇文字」的動作一樣,這時cursor會變成「|」,也就是選擇文字會出現的cursor。這其實是因為事件的預設動作在作祟,所以只要使用preventDefault()防止這個動作發生就可以解決。另外,除了呼叫preventDefault(),其實在事件處理函數最後return false也有一樣的效果。

接下來看一下事件處理的方式,除了事件處理函數,其實還需要用一個變數來存放目前的狀態。因為mousemove事件只要滑鼠通過就會觸發,這並不是我們希望的動作。透過設定一個drawing變數,讓他初始時是false,在mousedown時變成true,mouseup時再回到false,而繪製的動作只在drawing==true時發生,這樣就可以避免只要滑鼠移過Canvs就會畫出圖形。另外,為了處理滑鼠在繪製中移到Canvas外才mouseup的狀況,利用mouseleave事件做跟mouseup一樣的處理,這樣才不會在滑鼠在Canvas外mouseup後移回Canvas卻繼續繪製。

 	canvas.bind('mousedown', function(e) {
 		e.preventDefault();
 		if(!drawing) {
 			$(this).data('cursor', $(this).css('cursor'));
 			$(this).css('cursor', 'pointer');
 			drawing = true;
 			var offset = $(e.currentTarget).offset()
 			var x = e.pageX - offset.left;
 			var y = e.pageY - offset.top;
 			drawLine(context,x,y,x,y);
 			queue.push([x,y]);
 		}
 	});
 	canvas.bind('mousemove', function(e) {
 		e.preventDefault();
 		if(drawing) {
 			var old = queue.shift();
 			var offset = $(e.currentTarget).offset();
 			var x = e.pageX - offset.left;
 			var y = e.pageY - offset.top;
 			drawLine(context,old[0],old[1],x,y);
 			queue.push([x,y]);
 		}
 	});
 	canvas.bind('mouseup', function(e) {
 		if(drawing) {
 			$(this).css('cursor', $(this).data('cursor'));
 			var old = queue.shift();
 			var offset = $(e.currentTarget).offset();
 			var x = e.pageX - offset.left;
 			var y = e.pageY - offset.top;
 			drawLine(context,old[0], old[1], x, y);
 			drawing = false;
 		}
 	});
 	canvas.bind('mouseleave', function(e) {
 		if(drawing) {
 			$(this).css('cursor', $(this).data('cursor'));
 			var old = queue.shift();
 			var offset = $(e.currentTarget).offset();
 			var x = e.pageX - offset.left;
 			var y = e.pageY - offset.top;
 			drawLine(context,old[0], old[1], x, y);
 			drawing = false;
 		}
 	});

另外一個必須注意的點,是座標的計算。我是利用jquery event物件的pageX / pageY取出事件發生時,滑鼠相對於頁面左上角的座標,與利用offset()取得的Canvas左上角座標相減,來取得滑鼠相對於Canvas的座標。

不過這樣畫圖,用的都是預設的顏色、線段樣式與線段寬度,有點無趣,所以再加上一些功能。

自己刻一個工具列需要花一些時間。為了快速測試,就先拿現成的color picker來選擇顏色,用下拉選單來挑選線段的寬度。

為了讓他盡量簡單,我挑了一個最簡單的jQuery外掛:http://laktek.com/2008/10/27/really-simple-color-picker-in-jquery/

依照說明把它加到網頁中,然後在呼叫colorPicker來載入時丟給他的物件中,設定onColorChange屬性,用一個函數來接收顏色的變化值:

 	$('#color').colorPicker({
 		onColorChange: function(id, val) {
 			context.strokeStyle = val;
 		}
 	});

真的很簡單,只要一行程式就做完了XD

另外在加一個選單來改變線段的寬度,先把利用onchange事件來改變:

 	$('#linewidth').bind('change', function() {
 		context.lineWidth = $(this).val();
 	})

這時會發現另一個問題,在線段變粗時才會明顯,就是線段的接合處不太對勁...竟然跑出縫隙XD...這時只好繼續調整,把lineJoin屬性改成'round',同時也把lineCap也改成'round',這樣可以把線段端點變成圓形,接合的方式也變成圓形,這樣就不會出現縫隙了。

另外,其實strokeStyle除了指定顏色,還可以指定漸層物件或pattern物件,不過剛剛試一下好像沒作用,還是到明天測試圖形繪製的時候再用填色(fill)來試試看。預計隨著功能越加越多,目前的作法會讓程式變得雜亂不好維護,也開始思考一下怎麼把功能組織起來。

今天完整的程式如下,左側會繼續發展成工具列。

 <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;
 }
 </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>
 function EventEmitter() {
 	this.events = {};
 }
 EventEmitter.prototype.on = function(name, cb) {
 	if(typeof this.events[name] !== 'undefined') {
 		this.events[name] = [];
 	}
 	this.events[name].push(cb);
 };
 EventEmitter.prototype.emit = function() {
 	var self = this;
 	if(arguments.length<1) {
 
 	}
 }
 
 $(document).ready(function() {
 	var canvas = $('#canvas');
 	var context = canvas[0].getContext('2d');
 	var drawing = false;
 	var queue = [];
 	context.lineCap = 'round';
 	context.lineJoin = 'round';
 	$('#color').colorPicker({
 		onColorChange: function(id, val) {
 			context.strokeStyle = val;
 		}
 	});
 
 	canvas.bind('mousedown', function(e) {
 		e.preventDefault();
 		if(!drawing) {
 			$(this).data('cursor', $(this).css('cursor'));
 			$(this).css('cursor', 'pointer');
 			drawing = true;
 			var offset = $(e.currentTarget).offset()
 			var x = e.pageX - offset.left;
 			var y = e.pageY - offset.top;
 			drawLine(context,x,y,x,y);
 			queue.push([x,y]);
 		}
 	});
 	canvas.bind('mousemove', function(e) {
 		e.preventDefault();
 		if(drawing) {
 			var old = queue.shift();
 			var offset = $(e.currentTarget).offset();
 			var x = e.pageX - offset.left;
 			var y = e.pageY - offset.top;
 			drawLine(context,old[0],old[1],x,y);
 			queue.push([x,y]);
 		}
 	});
 	canvas.bind('mouseup', function(e) {
 		if(drawing) {
 			$(this).css('cursor', $(this).data('cursor'));
 			var old = queue.shift();
 			var offset = $(e.currentTarget).offset();
 			var x = e.pageX - offset.left;
 			var y = e.pageY - offset.top;
 			drawLine(context,old[0], old[1], x, y);
 			drawing = false;
 		}
 	});
 	canvas.bind('mouseleave', function(e) {
 		if(drawing) {
 			$(this).css('cursor', $(this).data('cursor'));
 			var old = queue.shift();
 			var offset = $(e.currentTarget).offset();
 			var x = e.pageX - offset.left;
 			var y = e.pageY - offset.top;
 			drawLine(context,old[0], old[1], x, y);
 			drawing = false;
 		}
 	});
 	$('#linewidth').bind('change', function() {
 		context.lineWidth = $(this).val();
 	});
 	function drawLine(ctx, x, y, x1, y1) {
 		ctx.beginPath();
 		ctx.moveTo(x,y);
 		ctx.lineTo(x1,y1);
 		ctx.closePath();
 		ctx.stroke();
 	}
 });
 </script>
 
 
 <div class='panel'>
 	<div class='tools' id='tools'>
 		<div>
 			<input type='text' id='color' name='color' value='#336699' style="float:left">
 			<select id='linewidth'>
 				<option value="1">1</option>
 				<option value="3">3</option>
 				<option value="5">5</option>
 				<option value="7">7</option>
 			</select>
 		</div>
 	</div>
 	<canvas id="canvas" width='640' height='480'></canvas>
 </div>
 
 

執行的畫面看起來像這樣:


上一篇
且戰且走HTML5(10) Canvas 及 2D context可以做到的事情
下一篇
且戰且走HTML5(12) Canvas基本繪圖-圖形
系列文
且戰且走HTML530

尚未有邦友留言

立即登入留言