HTML5之Canvas 2D入門3
知識準備 - 座標系
在真正開始總結變換之前,我們需要先了解一下canvas的相關座標系知識。
“畫素座標系”:在HTML中,我們會設定canvas的屬性:width和height,它們是以畫素為單位的,它們描述了canvas最終的呈現區域,我形象稱之為“畫素座標”(自創,不是很貼切,行家別見笑),這個座標系原點在canvas的左上角,這個座標系當canvas建立完成以後,就不會變了(當然了,修改width與height的時候會變的),原點一直位於左上角;x與y各有多少畫素,都已經由width和height決定了。說白了,這個東西就像畫畫時的畫布,你給多大就多大。
“網格座標系”:在繪圖的時候使用的座標系。我們繪圖時所有的單位使用的並不是畫素座標,而是這個稱為網格的座標系。為了在有限的畫布內,畫出各種比例的圖形,我們可能就要對這個座標系進行各種變換(平移、旋轉、縮放)。所以後面總結的各種變換都是針對網格座標系的。
這兩個座標系的關係其實就像顯示器與桌面的關係一樣,顯示器就相當於畫素座標系,它的點都是固定的,造出來什麼樣就什麼樣。桌面就像是網格座標系,我們可以隨時移動,旋轉桌面,修改桌面解析度來看更多的內容。
在canvas中,預設情況下,網格座標與畫素座標是一一對應的,原點都在左上角,每1個網格單位對應1個畫素單位。canvas裡的所有物體的位置都是相對於網格座標的原點而言的。如下面圖中所示,預設情況下,藍色方塊的位置就是距左邊x單位和距上邊y單位(座標(x, y))。
知識準備 - 狀態保持
在正式介紹變形之前,還需要先了解一下兩個繪製複雜圖形就必不可少的方法,這兩個方法在變形中應用的相當廣泛。
儲存狀態:context.save()
恢復狀態:context.restore()
save和restore方法是用來儲存和恢復canvas狀態的,都沒有引數。canvas的狀態就是當前畫面應用的所有樣式和變形的一個快照。
canvas狀態是以堆(stack)的方式儲存的,每一次呼叫save方法,當前的狀態就會被推入堆中儲存起來。這種狀態包括:
•當前應用的變形(即移動,旋轉和縮放);
•所有樣式:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation 的值;
•當前的裁切路徑(clipping path)。
你可以呼叫任意多次save方法,將canvas狀態入棧。每一次呼叫restore方法,上一個儲存的狀態就從堆中彈出,所有設定都恢復。
下面的例子能很容易的說明save與restore的使用方法:
function draw() {
var ctx = document.getElementById('lesson01').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
}
在上面的例子中可以看到,如果每次都手動修改各個樣式的值,那將會很麻煩,特別是樣式的值很多的時候,更是容易出錯。這個時候使用save/restore還是很方便的。
變換
學過圖形學的都知道,變換有這麼幾種:移動,旋轉和縮放。為了弄清楚變換的效果,我們一定要理解,變換的目標是哪個。上面我也說了,這些變換都是針對網格座標系的。下面分別看一下這些變換。
平移變換:將網格座標系的原點移動指定的偏移量。
context.translate(x, y)
translate 方法接受兩個引數。x 是左右偏移量,y 是上下偏移量。
在做變形之前先儲存狀態是一個良好的習慣。大多數情況下,呼叫restore方法比手動恢復原先的狀態要簡單得多。特別是在迴圈中,更要注意儲存和恢復canvas的狀態。
旋轉變換:將網格座標系沿著自己的原點順時針旋轉指定的角度。
context.rotate(angle)
這個方法只接受一個引數:旋轉的角度(angle),它是順時針方向的,以弧度為單位的值。旋轉的中心點始終是 canvas 的原點。
縮放變換:將網格座標系的座標單位按照指定的比例進行縮小或放大。
context.scale(x, y)
scale 方法接受兩個引數。x,y 分別是橫軸和縱軸的縮放因子,它們都必須是正值。值比 1.0 小表示縮小,比 1.0 大則表示放大,值為 1.0 時什麼效果都沒有。
因為畫素大小是不變的,所以這個變換實際的效果就是同樣大小的畫布內,能畫的東西多了或少了。
變換例子如下:
function draw() {
var ctx = document.getElementById('lesson01').getContext('2d');
ctx.lineWidth = 1.5;
ctx.fillRect(0,0,300,300);
ctx.translate(150,150);
ctx.rotate(Math.PI/4);
ctx.scale(0.5,0.5);
ctx.clearRect(-40,-40, 80,80);
}
變換矩陣:所有的變換其實都可以用矩陣來表述。
可以用下面兩種方法直接設定變換矩陣:
context.transform(m11, m12, m21, m22, dx, dy)
context.setTransform(m11, m12, m21, m22, dx, dy)
第一個方法直接將當前的變形矩陣乘上下面的矩陣(注意排列的順序):
m11 m21 dx
m12 m22 dy
0 0 1
第二個方法會重置當前的變形矩陣為單位矩陣,然後以相同的引數呼叫transform方法。
function draw() {
var canvas = document.getElementById("lesson01");
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);
}
所有的變換起始都是通過變換矩陣實現的,所以上述的平移,旋轉,縮放都可以用相應的矩陣代替,精通數學的同學可以自己推匯出來:
平移:context.translate(dx,dy)可以使用context.transform (1,0,0,1,dx,dy)或者context.transform(0,1,1,0.dx,dy)代替。
旋轉:context.rotate(θ)可以使用context.transform(Math.cos(θ*Math.PI/180),Math.sin(θ*Math.PI/180),-Math.sin(θ*Math.PI/180),Math.cos(θ*Math.PI/180),0,0)或者
context.transform(-Math.sin(θ*Math.PI/180),Math.cos(θ*Math.PI/180),Math.cos(θ*Math.PI/180),Math.sin(θ*Math.PI/180), 0,0)代替。
縮放:context.scale(sx, sy)可以使用context.transform(sx,0,0,sy,0,0)或者context.transform(0,sy,sx,0, 0,0)代替。
組合
預設情況下,我們總是將一個圖形畫在另一個之上,也就是說繪製的結果受制於繪製圖形的順序。大多數情況下,這樣是不夠的,設定組合屬性就是解決圖形重疊時採取何種效果的問題。我們使用globalCompositeOperation屬性來改變預設做法。
globalCompositeOperation = type
type是下列的12中字串值之一:
•source-over (default):這是預設設定,新圖形會覆蓋在原有內容之上。
•source-in:新圖形會僅僅出現與原有內容重疊的部分。其它區域都變成透明的。
•ource-out:結果是隻有新圖形中與原有內容不重疊的部分會被繪製出來。
•source-atop:新圖形中與原有內容重疊的部分會被繪製,並覆蓋於原有內容之上。
•lighter:兩圖形中重疊部分作加色處理。
•xor:重疊的部分會變成透明。
•destination-over:會在原有內容之下繪製新圖形。
•destination-in:原有內容中與新圖形重疊的部分會被保留,其它區域都變成透明的。
•destination-out:原有內容中與新圖形不重疊的部分會被保留。
•destination-atop:原有內容中與新內容重疊的部分會被保留,並會在原有內容之下繪製新圖形。
•darker:兩圖形中重疊的部分作減色處理。
•copy:只有新圖形會被保留,其它都被清除掉。
假設我們先繪製了一個藍色的矩形,再繪製一個紅色的圓形,則應用這12種組合設定的結果如下所示:
注意:如果設定的屬性值沒有效果,說明目前您使用的瀏覽器還不支援該組合屬性值。
裁剪
與組合相關的一個問題是裁剪。其實在繪製路徑的時候,最後一步將圖形繪製到canvas的函式除了stroke和fill外,還有就是clip;以clip結束路徑時會將當前繪製的圖形當做裁剪路徑,只有在裁剪路徑內的圖形才會顯示。裁切路徑屬於canvas狀態的一部分,可以被儲存起來。如果我們在建立新裁切路徑時想保留原來的裁切路徑,我們需要做的就是儲存一下canvas的狀態。
例如下面的例子就是先繪製Mask層的背景和裁剪路徑,然後繪製的圖形就只有在裁剪路徑內的才可見:
function draw() {
var ctx = document.getElementById('lesson01').getContext('2d');
// draw mask background
ctx.fillRect(0,0,150,150);
ctx.translate(75,75);
// create a circular clipping path
ctx.beginPath();
ctx.arc(0,0,60,0,Math.PI*2,true);
ctx.clip();
// draw background
var lingrad = ctx.createLinearGradient(0,-75,0,75);
lingrad.addColorStop(0, '#232256');
lingrad.addColorStop(1, '#143778');
ctx.fillStyle = lingrad;
ctx.fillRect(-75,-75,150,150);
// draw stars
for (var j=1;j<50;j++){
ctx.save();
ctx.fillStyle = '#fff';
ctx.translate(75-Math.floor(Math.random()*150),
75-Math.floor(Math.random()*150));
drawStar(ctx,Math.floor(Math.random()*4)+2);
ctx.restore();
}
}
function drawStar(ctx,r){
ctx.save();
ctx.beginPath()
ctx.moveTo(r,0);
for (var i=0;i<9;i++){
ctx.rotate(Math.PI/5);
if(i%2 == 0) {
ctx.lineTo((r/0.525731)*0.200811,0);
} else {
ctx.lineTo(r,0);
}
}
ctx.closePath();
ctx.fill();
ctx.restore();
}