
- 前言 -
一路勇闖地下城到了第七層,因為自認文筆不好又詞窮,本來沒有計劃寫部落格的,但好死不死第七層遇到了我心中最軟的一塊 Canva…
初期進入前端領域時都著重在網頁切版與 JavaScript,認為自己應該不太會應用到 Canvas,所以一直沒有把它排進我的學習清單中,當然之後網站看多了,就有發現 Canvas 的應用其實非常豐富,幸好這次也有機會來好好學習一下了。
- 重點技術 -
- HTML5 Canvas 運用
- JavaScript / jQuery 程式設計
- BOSS 攻略-
所以,如何開始 ?
因為真的沒碰過,實在不知道從何開始,只知道 HTML 裡有 <canvas> 的標籤,但標籤裡要放些甚麼 ?
看了看 MDN ,原來需要取得 <canvas> 的渲染環境,用getContext() 輸入參數 2d 來取得 2D 的繪圖環境,這樣後面才能使用相關的繪圖函式。
let canvas = document.querySelector('#draw)
let ctx = canvas.getContext('2d')到這裡卻發現畫布只有小小一塊,這樣怎麼夠畫呢?雖然可以用 CSS 來調整 Canvas 的大小,但官方建議使用 <canvas> 的 width 和 height 來設定,不然在繪圖上會有比例的問題。
let setSize = function(){
canvasWidth = $(window).outerWidth(true)
canvasHeight = $(window).outerHeight(true)
$('#draw').attr('width',canvasWidth)
$('#draw').attr('height',canvasHeight)
}這樣就能根據視窗大小來設定 Canvas 的屬性了。
畫布有了,然後呢?
現在準備好一張全新的空白畫布了,但該如何在上面畫圖呢 ?先來看看該怎麼畫出直線吧 !
現在已經可以使用相關的繪圖函式了,畫直線的話需要三個基本的方法。
ctx.beginPath()
// 跟畫布說一聲我們要開始了。ctx.moveTo(X,Y)
// 跟畫布告知我們要從哪個座標開始。ctx.lineTo(X,Y)
// 最後告知我們的直線要延伸到哪個座標。
做完這三個步驟後卻發現畫布上甚麼也沒有,因為剛剛只做到了告知的動作,並還沒叫電腦實際畫出來。
ctx.stroke() 請電腦幫我們畫出直線吧!
我只有鉛筆嗎?
成功畫出第一筆後卻發現只有一條細細醜醜的黑線,畫筆的顏色粗細該怎麼調整呢?
ctx.strokeStyle = '#FFA500'
// 這樣就有橘色的畫筆了,CSS的顏色格式都適用喔。ctx.lineWidth = 10
// 畫筆的粗細的也可以調整了。ctx.lineCap = 'round'
// 讓筆畫圓潤一點吧!
這樣我們就有隨心所欲可調整的畫筆了 !

互動的開始
畫筆、畫布都有了,但總不能叫使用者自己打程式來畫圖吧?而且現在還只能畫直線…
為了讓使用者可以利用滑鼠來繪圖,必須要把上面的畫直線方法來跟滑鼠事件連動,而各種的筆畫線條實際上是用長度只有 1pixel 的直線連起來的。
let lastPointX, lastPointY
let downHandler =function(e){
// 滑鼠按下去時得到座標存在變數中作為等等畫圖的起點
lastPointX = e.offsetX
lastPointY = e.offsetY
// 並且為Canvas綁定mousemove和mouseup的事件
$('#draw').on('mousemove',moveHandler)
$('#draw').on('mouseup',upHandler)
}let moveHandler =function(e){
// 滑鼠在移動時我們把新的座標存下來作為終點
let newPointX = e.offsetX
let newPointY = e.offsetY // 剛剛說的畫圖動作
ctx.beginPath()
ctx.moveTo(lastPointX,lastPointY)
ctx.lineTo(newPointX,newPointY)
ctx.stroke() // 把終點改為新的起點
lastPointX = newPointX
lastPointY = newPointY
}let upHandler = function(){
// 滑鼠釋放後把剛剛綁定的事件移除
$('#draw').off('mousemove',moveHandler)
$('#draw').off('mouseup',upHandler)
}$('#draw').on('mousedown',downHandler)
簡單來說我們就是利用滑鼠移動的事件來不斷取得新座標,並且把座標丟進 moveTo() 和 lineTo() 之中,而滑鼠按下和放開只是啟動和關閉的作用。

進階的功能
有了基本的繪圖功能,來思考該如何達到「復原」和「重做」吧!
看了看文件,原本發現 Path2D Object 和 save(), restore() 好像蠻符合我們要的概念的。
Path2D Object 是利用 MyPath = new Path2D() 來建立一個路徑物件,可以事先存取路徑再利用 ctx.stroke(MyPath) 畫出來,但這個物件只能存取路徑卻無法存取畫筆顏色和樣式。
而save(), restore() 可以存取畫布狀態並重新呼叫來畫出,但一次只能存取一個狀態到 stack 中,看來也不適用。
只好看看大家怎麼做的,原來要利用toDataURL() 來「存入」和「讀取」。toDataURL() 可以幫我們把畫布狀態編碼為 base64 的字串,這樣我們就可以方便存取啦!
let step = -1
let history = []let push = function(){
step++;
if (step <= history.length - 1) history.length = step
history.push(canvas.toDataURL())
}// 記得將push()加入upHandler中...
先來定義兩個變數 step 用來紀錄步數 history 用來紀錄每一步的筆畫。每畫一筆步數就 +1,並且把base64 存進 history 中。
但為何中間要有一個判斷式呢?
我們來思考一下,下面是我們畫圖時的步驟A -> B -> C -> D step = 3
// 我們總共畫了四筆,分別都存進 history[A,B,C,D]A -> B -> C [D] step = 2
// 我們發現D畫錯了,所以復原到C,但D仍然是 history 裡的第[3]步A -> B -> C -> E [D] step = 3
// 這時候我們重畫了一個E,它是新的第[3]步,而舊的D必須被我們覆蓋掉
// history[A,B,C,E]這樣應該就很好理解了吧~
現在每一筆都被記錄下來了,那該怎麼把紀錄給呼叫回來呢?
let undo = function(){
// 創建一個新的圖像物件
let lastDraw = new Image // 確定有上一步我們才回到上一步
if(step > 0) step-- // 把上一部的base64設定給圖像物件
lastDraw.src = history[step] // 把圖片載入後用畫布選染出來
lastDraw.onload = function(){
ctx.clearRect(0,0,canvasWidth,canvasHeight)
ctx.drawImage(lastDraw, 0, 0)
}
}
這樣復原就完成了,而重做的概念剛好就是相反的囉。而其實裡面也藏了清除畫布的方法 ctx.clearRect(0,0,canvasWidth,canvasHeight),這樣 ClearAll就可以直接拔出來用了。
保存美好
經過復原重做,應該對保存有些靈感了吧? 沒錯,又是toDataURL() 。
$('#save').on('click',function(){
let link = canvas.toDataURL('image/png')
$(this).attr('href',link)
$(this).attr('download','canvas.png')
})在按下保存後,把畫布狀態利用toDataURL() 編碼並設定在連結中,這樣按下去後就可以下載囉~
這樣我們的繪圖板就完成啦!剩下顏色選擇和筆畫粗細的調整,相信對能到達第七層的勇者來說,肯定是一塊小蛋糕的!讓我們朝下一關邁進吧!!
- 番外篇 -
相信許多勇者在思考額外功能的時候都是朝著新增顏色的方向前進,而我也不例。
let brushColor = ['#ffffff','#000000','#9BFFCD','#00CC99','#01936F']利用陣列來管理顏色,只要使用者新增顏色,就會更新陣列資料,並且利用forEach() 來生成 DOM :
brushColor.forEach((item)=>{
let color = $(`<a class="color" href="javascript:;"></a>`)
let check = $(`<p><i class="fas fa-check"></i></p>`) color.css('background',item) if(isDark(item)) check.css('color','white') color.append(check)
})
而我主要想要分享的是判斷式中我呼叫的一個名為 isDark 的 function,這個函式是做什麼用的呢?

大家知道顏色有深有淺,但我的勾勾卻是黑色的,如果在深色上是看不清楚的,那我該如何知道使用者選的顏色是深是淺呢?
let isDark = function(color){
let rgbArray =
[color.substr(1,2),color.substr(3,2),color.substr(5,2)] let brightness =
parseInt(`0x${rgbArray[0]}`) * 0.213 +
parseInt(`0x${rgbArray[1]}`) * 0.715 +
parseInt(`0x${rgbArray[2]}`) * 0.072 return brightness < 255 / 2
}
因為陣列中的資料都是#FFFFFF 帶有井字號的16進位色碼,我們把字串拆開並且轉為10進位的數字,這樣我們就輕易的拿到顏色的 RGB 啦~~
最後用一個神秘的算式就能知道這個顏色是深是淺,而我們就能給他對應的白勾勾與黑勾勾了!
