Canvas-基本繪製與輸出圖片

一直都沒機會用到的 Canvas。

相關 methods

通用:

  • fillRect 畫出實心矩形
  • strokeRect 畫空心矩形
  • clearRect 清除畫面

通常會搭配一起使用的:

  • beginPath 開啟新的繪圖路徑
  • moveTo 初始位置
  • lineTo 結束位置
  • stroke 畫出線條軌跡
  • closePath 關閉繪圖路徑

地雷:同樣是 width / height,但 HTML 和 CSS 的意義不同

在使用 Canvas 時可能會想要設定畫布的寬跟高,所以可能會很直覺的用 CSS 來設定,不過這樣的寫法跟直接寫在 HTML 屬性上的意義不一樣,直接來看個範例:

1
2
3
4
5
6
7
8
9
10
11
12
<canvas id="canvas1" width="50" height="100"></canvas>
<canvas id="canvas2" width="50" height="100" style="width: 100px"></canvas>

<script>
ctx1 = canvas1.getContext('2d')
ctx2 = canvas2.getContext('2d')
ctx1.fillStyle = 'dodgerblue'
ctx2.fillStyle = 'dodgerblue'
// 在兩個 canvas 中都畫上同樣大小的方塊
ctx1.fillRect(10, 10, 30, 30)
ctx2.fillRect(10, 10, 30, 30)
</script>

注意這兩個 canvas 的內容幾乎一樣,只在差 canvas2 多在 CSS 中設定了 width: 100px,最後出來的結果如下:

example1

附註:Codepen

看到差異了嗎?canvas2 的藍色方塊直接被放大了一倍。這是因為 HTML 的寬高是用來設定 Canvas 內部的座標和網格系統,而 CSS 則是用來設定實際呈現在畫布上的實際大小。

以上面的例子來說的話,可以想成是 canvas2 在 Canvas 內部的原本大小是 50 x 100,但因為我們用 CSS 設定了 width: 100px,意思就是最後呈現的畫面必須從原本的 50 x 100 變成 100 x 50,也就是 2 倍(100 / 50),所以最後看到的畫面才會看起來放大了兩倍。

canvas1 因為沒有設定 CSS,所以預設會自動以 1:1 的方式去呈現畫布中的內容。

基本繪製功能

這邊只解釋大概思路,實作的部分就直接看程式碼吧:

  1. 建立一個 flag 來表示目前是否在繪圖,藉此來開關路徑的繪製
  2. 在畫布中「點擊」時,開啟繪圖狀態(更新 flag)
  3. 「移動」游標時,取得座標位置並繪製路徑(只有在繪圖狀態時才執行)
  4. 在對應的時機點結束繪製,一個是游標「離開」畫布時,一個是「鬆開」按鍵時

在電腦上必須監聽底下事件:

  • onMouseDown
  • onMouseMove
  • onMouseUp
  • onMouseLeave

在手機上必須監聽底下事件:

  • onTouchStart
  • onTouchMove
  • onTouchCancel
  • onTouchEnd
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
function App() {
const canvas1 = useRef<HTMLCanvasElement>(null)
// flag
const isPanting = useRef<boolean>(false)

useEffect(() => {
const ctx1 = canvas1.current!.getContext('2d')!
// 線條的 config
ctx1.lineWidth = 4
ctx1.lineCap = 'round'

return () => {
// 處理 React StrictMode 會渲染兩次的問題
ctx1.clearRect(0, 0, canvas.current!.width, canvas.current!.width)
}
}, [])

// 取得游標在畫布中的位置(座標)
function getStartCoord(event: React.TouchEvent | React.MouseEvent) {
const cavasSize = canvas1.current?.getBoundingClientRect()!
const position = {
x: 0,
y: 0
}

// pc / mobile
if (event.type === 'mousemove') {
position.x = (event as React.MouseEvent).clientX - cavasSize.left
position.y = (event as React.MouseEvent).clientY - cavasSize.top
} else {
position.x = (event as React.TouchEvent).changedTouches[0].clientX - cavasSize.left
position.y = (event as React.TouchEvent).changedTouches[0].clientY - cavasSize.top
}

return position
}

function onStart() {
// 開啟繪圖狀態
isPanting.current = true
}

function onDrawing(event: React.TouchEvent | React.MouseEvent) {
// 因為監聽的是 move,所以要避免在 canvas 範的圍外執行繪製
if (isPanting.current) {
const coord = getStartCoord(event)
const ctx1 = canvas1.current!.getContext('2d')!
ctx1.lineTo(coord.x, coord.y)
ctx1.stroke()
}
}

// 結束繪圖
function onFinish() {
const ctx1 = canvas1.current!.getContext('2d')!
// 記得重新開啟一個新的繪圖路徑,不然會影響到下一次的繪圖(重要!!!)
ctx1.beginPath()
// 更新 flag
isPanting.current = false
}

return (
<div className='App'>
<canvas
ref={canvas1}
id='canvas'
width='500'
height='300'
onMouseMove={onDrawing}
onMouseDown={onStart}
onMouseUp={onFinish}
onMouseLeave={onFinish}
onTouchStart={onStart}
onTouchMove={onDrawing}
onTouchCancel={onFinish}
onTouchEnd={onFinish}
></canvas>
</div>
)
}

example2-draw

附註:Codepen

產生圖片

Canvas 元素上有一個 toDataURL 的 method,可以用來把目前畫布上的內容轉換成「base64」的格式。如果你對 base64 不陌生的話,應該就會知道它其實可以當作 <img>src 值,這樣就可以顯示圖片了!

直接來看範例:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function App() {
const canvas = useRef<HTMLCanvasElement>(null)
const [imgSrc, setImgSrc] = useState<string>('')
const isPanting = useRef<boolean>(false) // flag

// ...略,這邊都跟之前的範例一樣

// 清除畫布
function onClear() {
const ctx = canvas.current!.getContext('2d')!
ctx.clearRect(0, 0, canvas.current!.width, canvas.current!.width)
setImgSrc('')
}

// 輸出圖片
function onSave() {
// 把目前畫布上的內容轉成 base64 格式
const base64 = canvas.current!.toDataURL('image/png')
setImgSrc(base64)
}

return (
<div className='App'>
<div className='wrapper'>
<canvas
ref={canvas}
id='canvas'
width='500'
height='300'
onMouseMove={onDrawing}
onMouseDown={onStart}
onMouseUp={onFinish}
onMouseLeave={onFinish}
onTouchStart={onStart}
onTouchMove={onDrawing}
onTouchCancel={onFinish}
onTouchEnd={onFinish}
></canvas>
<div className='img-wrapper'>
{/* 有 src 時才顯示圖片 */}
{imgSrc !== '' ? <img src={imgSrc} /> : <p>No imgae yet...</p>}
</div>
</div>
<div className='btns'>
<button onClick={onClear}>Reset</button>
<button onClick={onSave}>Save</button>
</div>
</div>
)
}

附註:Codepen

example3-image

Canvas-渲染 PDF 的方法 Gsap-ScrollTrigger
Your browser is out-of-date!

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

×