HTML5 canvas 元素詳細教程五 :上一節講了應用風格和顏色,這一節我們講canvas的變形。
狀態的保存和恢復 Saving and restoring state
在了解變形之前,我先介紹一下兩個你一旦開始繪制復雜圖形就必不可少的方法。
save()
restore()
save
和 restore
方法是用來保存和恢復 canvas 狀態的,都沒有參數。Canvas 的狀態就是當前畫面應用的所有樣式和變形的一個快照。
Canvas 狀態是以堆(stack)的方式保存的,每一次調用 save
方法,當前的狀態就會被推入堆中保存起來。這種狀態包括:
- 當前應用的變形(即移動,旋轉和縮放,見下)
strokeStyle
,fillStyle
,globalAlpha
,lineWidth
,lineCap
,lineJoin
,miterLimit
,shadowOffsetX
,shadowOffsetY
,shadowBlur
,shadowColor
,globalCompositeOperation 的值
- 當前的裁切路徑(clipping path),會在下一節介紹。
你可以調用任意多次
save
方法。每一次調用restore
方法,上一個保存的狀態就從堆中彈出,所有設定都恢復。
save 和 restore 的應用例子
我們嘗試用這個連續矩形的例子來描述 canvas 的狀態堆是如何工作的。
第一步是用默認設置畫一個大四方形,然后保存一下狀態。改變填充顏色畫第二個小一點的藍色四方形,然后再保存一下狀態。再次改變填充顏色繪制更小一點的半透明的白色q四方形。
到目前為止所做的動作和前面章節的都很類似。不過一旦我們調用 restore
,狀態堆中最后的狀態會彈出,并恢復所有設置。如果不是之前用 save
保存了狀態,那么我們就需要手動改變設置來回到前一個狀態,這個對于兩三個屬性的時候還是適用的,一旦多了,我們的代碼將會猛漲。
當第二次調用 restore
時,已經恢復到最初的狀態,因此最后是再一次繪制出一個黑色的四方形。
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 }
移動 Translating
來變形,我們先介紹
translate
方法,它用來移動 canvas 和它的原點到一個不同的位置。
translate(x, y)
translate
方法接受兩個參數。x 是左右偏移量,y 是上下偏移量,如右圖所示。
在做變形之前先保存狀態是一個良好的習慣。大多數情況下,調用 restore 方法比手動恢復原先的狀態要簡單得多。又,如果你是在一個循環中做位移但沒有保存和恢復 canvas 的狀態,很可能到最后會發現怎么有些東西不見了,那是因為它很可能已經超出 canvas 范圍以外了。
translate
的例子
這個例子顯示了一些移動 canvas 原點的好處。我創建了一個
drawSpirograph
方法用來繪制螺旋(spirograph)圖案,那是圍繞原點繪制出來的。如果不使用 translate
方法,那么只能看見其中的四分之一。translate
同時讓我可以任意放置這些圖案,而不需要在 spirograph 方法中手工調整坐標值,既好理解也方便使用。
我在 draw
方法中調用 drawSpirograph
方法 9 次,用了 2 層循環。每一次循環,先移動 canvas ,畫螺旋圖案,然后恢復早原始狀態。
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(); }
旋轉 Rotating
第二個介紹
rotate
方法,它用于以原點為中心旋轉 canvas。
rotate(angle)
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).
這個方法只接受一個參數:旋轉的角度(angle),它是順時針方向的,以弧度為單位的值。
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.
旋轉的中心點始終是 canvas 的原點,如果要改變它,我們需要用到 translate
方法。
rotate
的例子
在這個例子里,見右圖,我用
rotate
方法來畫圓并構成圓形圖案。當然你也可以分別計算出 x和 y 坐標(x = r*Math.cos(a); y = r*Math.sin(a)
)。這里無論用什么方法都無所謂的,因為我們畫的是圓。計算坐標的結果只是旋轉圓心位置,而不是圓本身。即使用 rotate
旋轉兩者,那些圓看上去還是一樣的,不管它們繞中心旋轉有多遠。
這里我們又用到了兩層循環。第一層循環決定環的數量,第二層循環決定每環有多少個點。每環開始之前,我都保存一下 canvas 的狀態,這樣恢復起來方便。每次畫圓點,我都以一定夾角來旋轉 canvas,而這個夾角則是由環上的圓點數目的決定的。最里層的環有 6 個圓點,這樣,每次旋轉的夾角就是 360/6 = 60 度。往外每一環的圓點數目是里面一環的 2 倍,那么每次旋轉的夾角隨之減半。
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
接著是縮放。我們用它來增減圖形在 canvas 中的像素數目,對形狀,位圖進行縮小或者放大。
scale(x, y)
scale
方法接受兩個參數。x,y 分別是橫軸和縱軸的縮放因子,它們都必須是正值。值比 1.0 小表示縮小,比 1.0 大則表示放大,值為 1.0 時什么效果都沒有。
默認情況下,canvas 的 1 單位就是 1 個像素。舉例說,如果我們設置縮放因子是 0.5,1 個單位就變成對應 0.5 個像素,這樣繪制出來的形狀就會是原先的一半。同理,設置為 2.0 時,1 個單位就對應變成了 2 像素,繪制的結果就是圖形放大了 2 倍。
scale
的例子
這最后的例子里,我再次啟用前面曾經用過的 spirograph 方法,來畫 9 個圖形,分別賦予不同的縮放因子。左上角的圖形是未經縮放的。黃色圖案從左到右應用了統一的縮放因子(x 和 y 參數值是一致的)。看下面的代碼,你可以發現,我在畫第二第三個圖案時
scale
了兩次,中間沒有 restore canvas 的狀態,因此第三個圖案的縮放因子其實是 0.75 × 0.75 = 0.5625。
第二行藍色圖案堆垂直方向應用了不統一的縮放因子,每個圖形 x 方向上的縮放因子都是 1.0,意味著不縮放,而 y 方向縮放因子是 0.75,得出來的結果是,圖案被依次壓扁了。原來的圓形圖案變成了橢圓,如果細心觀察,還可以發現在垂直方向上的線寬也減少了。
第三行的綠色圖案與第二行類似,只是縮放限定在橫軸方向上了。
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
最后一個方法是允許直接對變形矩陣作修改。
transform(m11, m12, m21, m22, dx, dy)
這個方法必須將當前的變形矩陣乘上下面的矩陣:
m11 m21 dx m12 m22 dy 0 0 1
如果任意一個參數是無限大,變形矩陣也必須被標記為無限大,否則會拋出異常。
setTransform(m11, m12, m21, m22, dx, dy)
這個方法必須重置當前的變形矩陣為單位矩陣,然后以相同的參數調用 transform
方法。如果任意一個參數是無限大,那么變形矩陣也必須被標記為無限大,否則會拋出異常。
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); }