要製作一個簡單的繪圖應用,自由塗鴉應該是最基本的功能之一。所以就先來看一下怎麼做出來。
塗鴉其實只使用到2D Context的幾個操作方法,主要是lineTo與滑鼠事件的搭配。lineTo是路徑的操作,要構成路徑,除了lineTo,還需要與moveTo、beginPath、closePath等方法做搭配。結束後,才用stroke來真正繪製出線條。
整個過程大概是像這樣:
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;
}
});
真的很簡單,只要一行程式就做完了
另外在加一個選單來改變線段的寬度,先把利用onchange事件來改變:
$('#linewidth').bind('change', function() {
context.lineWidth = $(this).val();
})
這時會發現另一個問題,在線段變粗時才會明顯,就是線段的接合處不太對勁...竟然跑出縫隙...這時只好繼續調整,把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>
執行的畫面看起來像這樣: