|
Posted on 2012-04-15 16:37 zljpp 閱讀(872) 評(píng)論(0) 編輯 收藏
HTML5 canvas 元素詳細(xì)教程五 :上一節(jié)講了應(yīng)用風(fēng)格和顏色,這一節(jié)我們講canvas的變形。
狀態(tài)的保存和恢復(fù) Saving and restoring state
在了解變形之前,我先介紹一下兩個(gè)你一旦開(kāi)始繪制復(fù)雜圖形就必不可少的方法。
save 和 restore 方法是用來(lái)保存和恢復(fù) canvas 狀態(tài)的,都沒(méi)有參數(shù)。Canvas 的狀態(tài)就是當(dāng)前畫(huà)面應(yīng)用的所有樣式和變形的一個(gè)快照。
Canvas 狀態(tài)是以堆(stack)的方式保存的,每一次調(diào)用 save 方法,當(dāng)前的狀態(tài)就會(huì)被推入堆中保存起來(lái)。這種狀態(tài)包括:
save 和 restore 的應(yīng)用例子

我們嘗試用這個(gè)連續(xù)矩形的例子來(lái)描述 canvas 的狀態(tài)堆是如何工作的。
第一步是用默認(rèn)設(shè)置畫(huà)一個(gè)大四方形,然后保存一下?tīng)顟B(tài)。改變填充顏色畫(huà)第二個(gè)小一點(diǎn)的藍(lán)色四方形,然后再保存一下?tīng)顟B(tài)。再次改變填充顏色繪制更小一點(diǎn)的半透明的白色q四方形。
到目前為止所做的動(dòng)作和前面章節(jié)的都很類似。不過(guò)一旦我們調(diào)用 restore ,狀態(tài)堆中最后的狀態(tài)會(huì)彈出,并恢復(fù)所有設(shè)置。如果不是之前用 save 保存了狀態(tài),那么我們就需要手動(dòng)改變?cè)O(shè)置來(lái)回到前一個(gè)狀態(tài),這個(gè)對(duì)于兩三個(gè)屬性的時(shí)候還是適用的,一旦多了,我們的代碼將會(huì)猛漲。
當(dāng)?shù)诙握{(diào)用 restore 時(shí),已經(jīng)恢復(fù)到最初的狀態(tài),因此最后是再一次繪制出一個(gè)黑色的四方形。
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.fillRect(0,0,150,150); // Draw a rectangle with default settings
ctx.save(); // Save the default state
ctx.fillStyle = '#09F' // Make changes to the settings
ctx.fillRect(15,15,120,120); // Draw a rectangle with new settings
ctx.save(); // Save the current state
ctx.fillStyle = '#FFF' // Make changes to the settings
ctx.globalAlpha = 0.5;
ctx.fillRect(30,30,90,90); // Draw a rectangle with new settings
ctx.restore(); // Restore previous state
ctx.fillRect(45,45,60,60); // Draw a rectangle with restored settings
ctx.restore(); // Restore original state
ctx.fillRect(60,60,30,30); // Draw a rectangle with restored settings
}
移動(dòng) Translating
來(lái)變形,我們先介紹 translate 方法,它用來(lái)移動(dòng) canvas 和它的原點(diǎn)到一個(gè)不同的位置。
translate 方法接受兩個(gè)參數(shù)。x 是左右偏移量,y 是上下偏移量,如右圖所示。
在做變形之前先保存狀態(tài)是一個(gè)良好的習(xí)慣。大多數(shù)情況下,調(diào)用 restore 方法比手動(dòng)恢復(fù)原先的狀態(tài)要簡(jiǎn)單得多。又,如果你是在一個(gè)循環(huán)中做位移但沒(méi)有保存和恢復(fù) canvas 的狀態(tài),很可能到最后會(huì)發(fā)現(xiàn)怎么有些東西不見(jiàn)了,那是因?yàn)樗芸赡芤呀?jīng)超出 canvas 范圍以外了。
translate 的例子
這個(gè)例子顯示了一些移動(dòng) canvas 原點(diǎn)的好處。我創(chuàng)建了一個(gè) drawSpirograph 方法用來(lái)繪制螺旋(spirograph)圖案,那是圍繞原點(diǎn)繪制出來(lái)的。如果不使用 translate 方法,那么只能看見(jiàn)其中的四分之一。translate 同時(shí)讓我可以任意放置這些圖案,而不需要在 spirograph 方法中手工調(diào)整坐標(biāo)值,既好理解也方便使用。
我在 draw 方法中調(diào)用 drawSpirograph 方法 9 次,用了 2 層循環(huán)。每一次循環(huán),先移動(dòng) canvas ,畫(huà)螺旋圖案,然后恢復(fù)早原始狀態(tài)。
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.fillRect(0,0,300,300);
for (var i=0;i<3;i++) {
for (var j=0;j<3;j++) {
ctx.save();
ctx.strokeStyle = "#9CFF00";
ctx.translate(50+j*100,50+i*100);
drawSpirograph(ctx,20*(j+2)/(j+1),-8*(i+3)/(i+1),10);
ctx.restore();
}
}
}
function drawSpirograph(ctx,R,r,O){
var x1 = R-O;
var y1 = 0;
var i = 1;
ctx.beginPath();
ctx.moveTo(x1,y1);
do {
if (i>20000) break;
var x2 = (R+r)*Math.cos(i*Math.PI/72) - (r+O)*Math.cos(((R+r)/r)*(i*Math.PI/72))
var y2 = (R+r)*Math.sin(i*Math.PI/72) - (r+O)*Math.sin(((R+r)/r)*(i*Math.PI/72))
ctx.lineTo(x2,y2);
x1 = x2;
y1 = y2;
i++;
} while (x2 != R-O && y2 != 0 );
ctx.stroke();
}
旋轉(zhuǎn) Rotating
第二個(gè)介紹 rotate 方法,它用于以原點(diǎn)為中心旋轉(zhuǎn) canvas。
This method only takes one parameter and that's the angle the canvas is rotated by. This is a clockwise rotation measured in radians (illustrated in the image on the right).
這個(gè)方法只接受一個(gè)參數(shù):旋轉(zhuǎn)的角度(angle),它是順時(shí)針?lè)较虻模曰《葹閱挝坏闹怠?/p>
The rotation center point is always the canvas origin. To change the center point, we will need to move the canvas by using the translate method.
旋轉(zhuǎn)的中心點(diǎn)始終是 canvas 的原點(diǎn),如果要改變它,我們需要用到 translate 方法。
rotate 的例子
在這個(gè)例子里,見(jiàn)右圖,我用 rotate 方法來(lái)畫(huà)圓并構(gòu)成圓形圖案。當(dāng)然你也可以分別計(jì)算出 x和 y 坐標(biāo)(x = r*Math.cos(a); y = r*Math.sin(a) )。這里無(wú)論用什么方法都無(wú)所謂的,因?yàn)槲覀儺?huà)的是圓。計(jì)算坐標(biāo)的結(jié)果只是旋轉(zhuǎn)圓心位置,而不是圓本身。即使用 rotate 旋轉(zhuǎn)兩者,那些圓看上去還是一樣的,不管它們繞中心旋轉(zhuǎn)有多遠(yuǎn)。
這里我們又用到了兩層循環(huán)。第一層循環(huán)決定環(huán)的數(shù)量,第二層循環(huán)決定每環(huán)有多少個(gè)點(diǎn)。每環(huán)開(kāi)始之前,我都保存一下 canvas 的狀態(tài),這樣恢復(fù)起來(lái)方便。每次畫(huà)圓點(diǎn),我都以一定夾角來(lái)旋轉(zhuǎn) canvas,而這個(gè)夾角則是由環(huán)上的圓點(diǎn)數(shù)目的決定的。最里層的環(huán)有 6 個(gè)圓點(diǎn),這樣,每次旋轉(zhuǎn)的夾角就是 360/6 = 60 度。往外每一環(huán)的圓點(diǎn)數(shù)目是里面一環(huán)的 2 倍,那么每次旋轉(zhuǎn)的夾角隨之減半。
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.translate(75,75);
for (var i=1;i<6;i++){ // Loop through rings (from inside to out)
ctx.save();
ctx.fillStyle = 'rgb('+(51*i)+','+(255-51*i)+',255)';
for (var j=0;j<i*6;j++){ // draw individual dots
ctx.rotate(Math.PI*2/(i*6));
ctx.beginPath();
ctx.arc(0,i*12.5,5,0,Math.PI*2,true);
ctx.fill();
}
ctx.restore();
}
}
縮放 Scaling
接著是縮放。我們用它來(lái)增減圖形在 canvas 中的像素?cái)?shù)目,對(duì)形狀,位圖進(jìn)行縮小或者放大。
scale 方法接受兩個(gè)參數(shù)。x,y 分別是橫軸和縱軸的縮放因子,它們都必須是正值。值比 1.0 小表示縮小,比 1.0 大則表示放大,值為 1.0 時(shí)什么效果都沒(méi)有。
默認(rèn)情況下,canvas 的 1 單位就是 1 個(gè)像素。舉例說(shuō),如果我們?cè)O(shè)置縮放因子是 0.5,1 個(gè)單位就變成對(duì)應(yīng) 0.5 個(gè)像素,這樣繪制出來(lái)的形狀就會(huì)是原先的一半。同理,設(shè)置為 2.0 時(shí),1 個(gè)單位就對(duì)應(yīng)變成了 2 像素,繪制的結(jié)果就是圖形放大了 2 倍。
scale 的例子
這最后的例子里,我再次啟用前面曾經(jīng)用過(guò)的 spirograph 方法,來(lái)畫(huà) 9 個(gè)圖形,分別賦予不同的縮放因子。左上角的圖形是未經(jīng)縮放的。黃色圖案從左到右應(yīng)用了統(tǒng)一的縮放因子(x 和 y 參數(shù)值是一致的)。看下面的代碼,你可以發(fā)現(xiàn),我在畫(huà)第二第三個(gè)圖案時(shí) scale 了兩次,中間沒(méi)有 restore canvas 的狀態(tài),因此第三個(gè)圖案的縮放因子其實(shí)是 0.75 × 0.75 = 0.5625。
第二行藍(lán)色圖案堆垂直方向應(yīng)用了不統(tǒng)一的縮放因子,每個(gè)圖形 x 方向上的縮放因子都是 1.0,意味著不縮放,而 y 方向縮放因子是 0.75,得出來(lái)的結(jié)果是,圖案被依次壓扁了。原來(lái)的圓形圖案變成了橢圓,如果細(xì)心觀察,還可以發(fā)現(xiàn)在垂直方向上的線寬也減少了。
第三行的綠色圖案與第二行類似,只是縮放限定在橫軸方向上了。
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.strokeStyle = "#fc0";
ctx.lineWidth = 1.5;
ctx.fillRect(0,0,300,300);
// Uniform scaling
ctx.save()
ctx.translate(50,50);
drawSpirograph(ctx,22,6,5); // no scaling
ctx.translate(100,0);
ctx.scale(0.75,0.75);
drawSpirograph(ctx,22,6,5);
ctx.translate(133.333,0);
ctx.scale(0.75,0.75);
drawSpirograph(ctx,22,6,5);
ctx.restore();
// Non-uniform scaling (y direction)
ctx.strokeStyle = "#0cf";
ctx.save()
ctx.translate(50,150);
ctx.scale(1,0.75);
drawSpirograph(ctx,22,6,5);
ctx.translate(100,0);
ctx.scale(1,0.75);
drawSpirograph(ctx,22,6,5);
ctx.translate(100,0);
ctx.scale(1,0.75);
drawSpirograph(ctx,22,6,5);
ctx.restore();
// Non-uniform scaling (x direction)
ctx.strokeStyle = "#cf0";
ctx.save()
ctx.translate(50,250);
ctx.scale(0.75,1);
drawSpirograph(ctx,22,6,5);
ctx.translate(133.333,0);
ctx.scale(0.75,1);
drawSpirograph(ctx,22,6,5);
ctx.translate(177.777,0);
ctx.scale(0.75,1);
drawSpirograph(ctx,22,6,5);
ctx.restore();
}
變形 Transforms
最后一個(gè)方法是允許直接對(duì)變形矩陣作修改。
transform(m11, m12, m21, m22, dx, dy)
這個(gè)方法必須將當(dāng)前的變形矩陣乘上下面的矩陣:
m11 m21 dx
m12 m22 dy
0 0 1
如果任意一個(gè)參數(shù)是無(wú)限大,變形矩陣也必須被標(biāo)記為無(wú)限大,否則會(huì)拋出異常。
setTransform(m11, m12, m21, m22, dx, dy)
這個(gè)方法必須重置當(dāng)前的變形矩陣為單位矩陣,然后以相同的參數(shù)調(diào)用 transform 方法。如果任意一個(gè)參數(shù)是無(wú)限大,那么變形矩陣也必須被標(biāo)記為無(wú)限大,否則會(huì)拋出異常。
transform / setTransform 的例子

function draw() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var sin = Math.sin(Math.PI/6);
var cos = Math.cos(Math.PI/6);
ctx.translate(200, 200);
var c = 0;
for (var i=0; i <= 12; i++) {
c = Math.floor(255 / 12 * i);
ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
ctx.fillRect(0, 0, 100, 10);
ctx.transform(cos, sin, -sin, cos, 0, 0);
}
ctx.setTransform(-1, 0, 0, 1, 200, 200);
ctx.fillStyle = "rgba(255, 128, 255, 0.5)";
ctx.fillRect(0, 50, 100, 100);
}
|