只能畫長方形其實有點無趣,至少加上圓形跟橢圓形才能讓圖形更完整。另外,為了顯示一些繪圖工具的狀態,加入了自定事件的機制。
圓形用2D Context的arc方法來做就可以做出來,只要做好畫圓形外框的功能,把方法改成填色,就變成畫實心圓的方法了。程式如下:
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);
可以看到,這裡是用昨天實作出來的PaintTools物件,來彈性地加入繪圖方法。
arc的前兩個參數其實是圓心,第三個是半徑,第四個是起始的角度,第五個是結束的角度,第六個是方向。簡單地說,就是指定圓心來畫弧線,依照指定的方向(順時針/逆時針)從開始的角度畫到結束的角度(相對於圓心)。所以只要從角度0畫到2 PI,就是一個圓了。
橢圓則可以利用2D Context提供的貝茲曲線來畫。為了簡化起見,一律只畫出水平的橢圓,這樣就可以用兩個點座標來定義出橢圓。先來看程式:
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);
每個貝茲曲線有兩個控制點,假設繪製起點的座標是(x1, y1),結束座標是(x2, y2),這樣就可以定義出從起點座標到結束座標的貝茲曲線的兩個控制點分別在(x1, y1-|y2-y1|)與(x2, y1-|y2-y1|),接下來從終點回到起點的被茲曲線的兩個控制點則是(x2, y1+|y2-y1|)與(x1, y1+|y2-y1|)。這樣畫出來的橢圓形(其實可能並不是,只是接近,不太確定這樣是否符合橢圓公式的定義,不過這是最快的作法),高度其實是比滑鼠拖拉時的y座標稍微低一些。
除了繪製圓形及橢圓形的方法外,為了更簡單地顯示繪圖的狀態,定義了一個EventEmitter來加入自定事件,並且稍微修改了PaintToolsProto中定義的getter/setter,在裡面觸發事件。利用這個方式,就可以在需要時讓PaintTools屬性改變時觸發事件。而且不必將事件與處理耦合。
完整的程式如下:
<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>
function EventEmitter() {
var handlers = {};
this.on = function(name, fn) {
if(typeof handlers[name] == 'undefined') handlers[name] = [];
handlers[name].push(fn);
};
this.emit = function() {
var name = arguments[0];
var args = [];
for(var i=1; i<arguments.length; i++) {
args.push(arguments[i]);
}
if(typeof handlers[name] !== 'undefined') {
handlers[name].forEach(function(fn) {
fn.apply(this, args);
})
}
};
}
</script>
<script>
(function(window, undefined) {
var document = window.document;
var tools = {}, queue = [], clones = [];
var PaintToolsProto = {
_drawType: '',
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);
}
};
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;
}
};
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();
}
}
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();
}
}
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();
}
}
})(window);
</script>
<script>
$(document).ready(function() {
// global variables
var canvas = $('#canvas');
var context = canvas[0].getContext('2d');
var drawing = false;
var queue = [];
var tool = $C(context);
// drawing tool setup
tool.on('drawType', function(v) {$('#drawtypestatus').html(v);});
tool.lineCap = 'round';
tool.lineJoin = 'round';
tool.drawType = 'freestyle';
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;
}
});
//drawing dom event
canvas.bind('mousedown', function(e) {
e.preventDefault();
if(!drawing) {
$(this).data('cursor', $(this).css('cursor'));
$(this).css('cursor', 'pointer');
drawing = true;
if(tool.drawType=='strokeRect' || tool.drawType=='fillRect' || tool.drawType=='strokeCircle' || tool.drawType=='fillCircle' || tool.drawType=='strokeEclipse' || tool.drawType=='fillEclipse') {
tool.pushCanvas();
}
var offset = $(e.currentTarget).offset()
var x = e.pageX - offset.left;
var y = e.pageY - offset.top;
tool.do(x,y,x,y);
queue.push([x,y]);
}
});
canvas.bind('mousemove', function(e) {
e.preventDefault();
if(drawing) {
var old = queue.shift();
if(tool.drawType=='strokeRect' || tool.drawType=='fillRect' || tool.drawType =='strokeCircle' || tool.drawType=='fillCircle' || tool.drawType=='strokeEclipse' || tool.drawType=='fillEclipse') {
tool.restoreCanvas();
queue.push(old);
}
var offset = $(e.currentTarget).offset();
var x = e.pageX - offset.left;
var y = e.pageY - offset.top;
tool.do(old[0],old[1],x,y);
if(tool.drawType == 'freestyle') {
queue.push([x,y]);
}
}
});
canvas.bind('mouseup', function(e) {
if(drawing) {
$(this).css('cursor', $(this).data('cursor'));
tool.dropCanvas();
var old = queue.shift();
var offset = $(e.currentTarget).offset();
var x = e.pageX - offset.left;
var y = e.pageY - offset.top;
tool.do(old[0], old[1], x, y);
drawing = false;
}
});
canvas.bind('mouseleave', function(e) {
if(drawing) {
$(this).css('cursor', $(this).data('cursor'));
tool.dropCanvas();
var old = queue.shift();
var offset = $(e.currentTarget).offset();
var x = e.pageX - offset.left;
var y = e.pageY - offset.top;
tool.do(old[0], old[1], x, y);
drawing = false;
}
});
// 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';
});
// define new drawtype and method
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);
});
</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>
<hr size='1' width='100%'>
</div>
</div>
<canvas id="canvas" width='640' height='480'></canvas>
</div>
操作畫面稍微調整過:
畫幾個圖試試看:
明天再來嘗試做出輸入文字的功能,這個地方不知道只用Canvas是否做不做得出來,如果不行,就需要用到一個文字輸入框,不過要把輸入框與Canvas文字對齊恐怕需要一番功夫。