JS地下城 - Canvas
確認需求
- 讓使用者可以使用滑鼠在畫布上繪圖
- 畫筆要可以進行基本設定(粗細、顏色)
- 一些畫布的基本功能(清空、復原、重做)
- 能夠將畫作下載為圖檔
另外還可以設計一些額外功能作為加分項目
解題攻略
# 準備畫布
先查查 MDN,發現原來要先建構 <canvas>
的渲染環境,用 getContext('2d')
來取得2D的繪圖環境,這樣後面才能使用相關的繪圖方法。
<canvas id="draw"></canvas>
let canvas = document.querySelector("#draw");
let ctx = canvas.getContext("2d");
在來要決定畫布的大小,以供我們畫圖,官方建議用 <canvas>
的 width
height
的屬性設定,避免用css修改,不然會有繪圖比例的問題。
這次的需求剛好是滿版的畫布,所以就這麼設定吧!
function setSize() {
let canvasWidth = window.innerWidth();
let canvasHeight = window.innerHeight();
canvas.setAttribute("width", canvasWidth);
canvas.setAttribute("height", canvasHeight);
}
準備好畫布後,就來簡單畫幾筆吧。
// 開始繪圖
ctx.beginPath();
// 設定起始座標
ctx.mobeTo(x, y);
// 設定終點座標
ctx.lineTo(x, y);
// 繪製
ctx.stroke();
藉由上面四個步驟就可以兩點連唯一線,畫出一條直線囉。
# 畫筆設定
成功畫出第一筆後卻發現只有一條細細醜醜的黑線,來試著改變畫筆的顏色粗細吧。
ctx.strokeStyle = "#FFA500";
ctx.lineWidth = 10;
ctx.lineCap = "round";
# 繪畫互動
畫筆、畫布都有了,但為了讓使用者可以利用滑鼠來繪圖,必須要把上面的畫直線方法來跟滑鼠事件連動。
let lastPointX, lastPointY;
let downHandler = function(e){
// 滑鼠按下去時得到座標存在變數中作為等等畫圖的起點
lastPointX = e.offsetX;
lastPointY = e.offsetY;
// 並且為Canvas綁定mousemove和mouseup的事件
draw.addEventListener("mousemove", moveHandler);
draw.addEventListener("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.removeEventListener("mousemove", moveHandler);
draw.removeEventListener("mouseup", upHandler);
};
draw.addEventListener("mousedown", downHandler)
透過鼠標的事件綁定,就可以做到類似小畫家的繪圖功能,而實際上畫出來的線條,其實是無數條 1pixel
的直線所串連起來的。
簡單來說就是利用滑鼠移動的事件來不斷取得新座標,並且把座標丟進 moveTo() 和 lineTo() 之中,而滑鼠按下和放開只是啟動和關閉的作用。
# 進階功能
有了基本的繪圖功能,來思考該如何達到「復原」和「重做」吧。
看了看文件,發現 Path2D Object
和 save()
restore()
好像蠻符合我們要的概念的。
不過仔細研究會發現:
- Path2D Object 是利用
MyPath = new Path2D()
來建立一個路徑物件,可以事先存取路徑再利用ctx.stroke(MyPath)
畫出來,但這個物件只能存取路徑卻無法存取畫筆顏色和樣式。 - 而
save()
restore()
可以存取畫布狀態並重新呼叫,但一次只能存取一個狀態到 stack 中,看來也不是我們需要的。
後來找到 toDataURL()
,它可以幫我們把畫布狀態編碼為 base64
的字串,這樣就可以存取了。
先來定義兩個變數 step 用來紀錄步數 history 用來紀錄每一步的筆畫。每畫一筆步數就 +1,並且把base64 存進 history 中。
let step = -1;
let history = [];
let push = function(){
step++;
if (step <= history.length - 1) history.length = step
history.push(canvas.toDataURL())
}
// 記得將push()加入upHandler中
但為何中間要有一個判斷式呢?我們來思考一下,下面是我們畫圖時的步驟:
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)
,這樣清除畫布的功能也一起做好了。
# 保存作品
實現復原重做後,保存其實也是一樣道理。
let save = document.querySelector("#save");
save.addEventListener("click", function() {
let link = canvas.toDataURL("image/png");
this.setAttribute("href", link);
this.setAttribute("download", "canvas.png");
})
在按下保存後,把畫布狀態利用 toDataURL()
編碼並設定在連結中,這樣可以下載了。
加分功能
另外也可以增加替換顏色的功能,先用陣列來管理顏色再利用 forEach()
來生成元素。
let brushColor = ["#ffffff","#000000","#9BFFCD","#00CC99","#01936F"];
不過顏色有深有淺,若打勾圖示固定是黑色的,那在較深的顏色上就會不清楚,所以我們要來判斷目前所選的顏色是深是淺。
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
}
先把16進位色碼拆開並轉為10進位數字,然後透過公式就能知道這個顏色是深是淺,也就能給予對應顏色的打勾圖示了。