1. 程式人生 > >HTML5之Canvas 2D入門3

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();  
}