把網頁當畫布揮灑的 Canvas 04:狀態儲存與還原

上一篇文章我們提到如何操作 Canvas 座標系來提升我們的繪圖效率。

今天,我們學習如何儲存還原畫布座標系的狀態,讓繪圖更有條理!


畫布狀態的保存與還原

簡單來說,畫布座標系狀態的保存與還原,讓我們能夠「還原至上一步」,就像製作簡報一樣,按下 Ctrl + Z 就能回到上一個動作。只是,在 Canvas 中,我們必須自行定義儲存的時機,畢竟是自己寫的程式嘛!

save()

保存當下畫布座標系的狀態。

1
ctx.save()

restore()

還原先前畫布座標系的狀態。

1
ctx.restore()

堆疊(Stack):後進先出

狀態的儲存與還原是以堆疊(Stack)的資料結構去執行的,符合「後進先出」(Last-In-First-Out)原則。我們可以把 Stack 想像成一個容器,最先放進去的會在最底部,所以最後才能拿出來執行,相反,最後放進去的會在最頂部,直接就能拿出來用。所以,才能夠「還原至上一個動作」

堆疊結構

順便複習一下,JavaScript 便是透過堆疊結構去執行函式的,請回頭參考〈weird-JavaScript 06:呼叫函式、執行堆疊〉


結合函式應用

由於畫布狀態的儲存與還原由我們自行定義,通常我們會讓儲存/還原成對呼叫,結合函式作用域的特性來協助我們繪製一個又一個互相獨立的圖形。如此一來,我們就不必擔心繪圖時混淆座標系狀態的問題。

1
2
3
4
5
6
7
8
9
10
function drawBlock(x, y, angle, frawFunc) {
ctx.save()
// 畫布想怎麼轉、怎麼移、怎麼縮放都沒關係!
ctx.translate(x, y)
ctx.rotate(angle)
// 透過傳入函式來繪製圖形
drawFunc()
// ...
ctx.restore() // 還原先前畫布狀態
}

如果你有跟著做上一篇文章中的範例,或許會發現,我們每畫一個 Block 之前,都要先透過 setTransform() 重置座標系,再 translate() 移動至該 Block 的繪圖位置。這樣的做法還是很不直覺,不過,現在我們學會如何儲存與還原畫布狀態,就可以結合函式改寫先前的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 使用不同顏色
const colors = {
red: '#ffa5a5',
blue: '#82cdff',
yellow: '#ffea9b',
green: '#48eaa1'
}

function drawBlock(pos, bgColor, fromCenter, drawFunc) {
ctx.save()
ctx.translate(pos.x * blockWidth, pos.y * blockWidth)
ctx.fillStyle = bgColor
ctx.fillRect(0, 0, blockWidth, blockWidth)
fromCenter ? ctx.translate(100, 100) : null
drawFunc()
ctx.restore()
}

// Block 1
drawBlock({x: 0, y: 0}, 'transparent', false, function() {
for(let i = 0 ; i < 10 ; i++) {
ctx.save()
for(let j = 0 ; j < 10 ; j++) {
ctx.fillStyle = i % 2 ? (j % 2 ? colors.blue : colors.red) : (j % 2 ? colors.yellow : colors.green)
ctx.fillRect(0, 0, 20, 20)
ctx.translate(20, 0)
}
ctx.restore() // 取代原本的 setTransform(1, 0, 0, 1, 0, 0)
ctx.translate(0, (i + 1) * 20)
}
})


自定義繪圖方法

如果我們很常繪製某一個圖形,可以將它自定義存進 ctx 中,以便隨時取用:

1
2
3
4
5
6
// 自定義繪製圓形
ctx.fillCircle = function(x, y, r) {
this.beginPath()
this.arc(x, y, r, 0, PI2)
this.fill()
}

當然,我們自行設計的繪圖,也可以封裝進函式中重複使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
drawDodecagon = function() {
ctx.beginPath()
ctx.moveTo(0, 80)
for(let i = 0 ; i < 12 ; i++) {
ctx.lineTo(0, 80)
ctx.rotate(PI2 / 12)
}
ctx.closePath()
ctx.lineWidth = 2.5
ctx.strokeStyle = 'white'
ctx.stroke()
}

drawBlock({x: 2, y: 0}, colors.yellow, true, drawDodecagon)
drawBlock({x: 0, y: 1}, colors.blue, true, drawDodecagon)
drawBlock({x: 2, y: 2}, colors.red, true, drawDodecagon)


參考資料

  1. 動畫互動網頁特效入門(JS/CANVAS):5-2 畫布的座標系操作
把網頁當畫布揮灑的 Canvas 05:物理基礎與dat-gui 把網頁當畫布揮灑的 Canvas 03:操作座標系

評論

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×