iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 8
0

任務目標

只要按住滑鼠就能在螢幕上畫畫, 線條大小, 顏色都會隨著畫筆移動的距離漸變. 像這樣

作法

<canvas>

<canvas>標籤是HTML5新增的元素, 透過<canvas>我們能夠以JS的程式碼輕易實現在瀏覽器繪圖, 製作動畫及影像處理等功能.

首先建立<canvas>標籤. canvas元素就像一塊作畫空間, width, height等屬性則用以初始化畫布大小. HTML程式碼如下:

<canvas id="draw" width="400" height="320"></canvas>

在JS裡面, 我們可以用canvas.width, canvas.height來存取width, height兩個屬性, 用來改變畫布大小.

這裡的canvas.width, 和canvas.style.width是不同的東西. canvas.width 指的是畫布內實際有多少像素寬, 如果canvas.width="400px", canvas.style.width="800px", 會產生一個像素解析度只有400px但體積占800px的畫布空間.

<canvas>元素內建getContext('2d)物件, 用以創造出渲染環境(context). 可以想像成小畫家的程式(canvas)打開後, 會看到畫布本體及其提供的作畫工具. 參數可設置為2d3d, 在這裡我們只需平面作畫, 將參數設為2d. JS程式碼如下:

const canvas = document.querySelector('#draw');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');

到此, 建立了一個畫布長寬與瀏覽器視窗相同的全屏畫布, 渲染環境為平面. 與平面作畫相關的工具都在getContext('2d')物件內. 因此特別將其存為一個變數, 方便我們呼喚渲染工具.

要讓滑鼠拖曳時就會畫畫, 需要建立監聽器. 架構大致如下:

// 初始化 isDrawing
let isDrawing = false;

function draw() {
  if(!isDrawing) return;
  // 畫畫
}

canvas.addEventListener('mousedown', (e) => {
	isDrawing = true;
});
canvas.addEventListener('mousemove', draw);	
canvas.addEventListener('mouseup', () => isDrawing = false);
canvas.addEventListener('mouseout', () => isDrawing = false);

首先分解一下'拖曳'這個動作, 拖曳就是'滑鼠有按'且'滑鼠有動', 也就是mousedownmousemove.

isDrawing代表是否正在畫畫, 觸發mousedown時將之設為true, 滑鼠按鍵放開(mouseup)或滑鼠移出元素範圍(mouseout), 將之設為false.

只要在滑鼠移動時(mousemove)觸發的回呼函式draw()中先判斷isDrawing是否為true, 再做動作, 就能達到'拖曳時就動作'的效果.

接下來是畫畫的函式了, 剛說到畫畫的工具都放在getContext('2d')內, 可以想成小畫家的畫筆, 在下筆前需要先選好筆的形狀, 線條粗細, 顏色等... 程式碼如下

// 初始化下筆點
let [lastX, lastY] = [0, 0];

function draw() {

    // 設定線條色彩
    ctx.strokeStyle = `red`;
    // 開始設定路徑
    ctx.beginPath();
    // 將畫筆移動到下筆座標點
    ctx.moveTo(lastX, lastY);
    // 從目前畫筆位置畫線到新座標點 (滑鼠所在位置)
    ctx.lineTo(e.offsetX, e.offsetY);

    // 上面都是在規劃路徑, 這裡才是將線畫出來
    ctx.stroke();
}

首先會看到所有功能都是附屬在getContext(‘2d)物件下的方法. strokeStyle用來決定畫線的顏色. beginPath()就是開始規劃路徑, 等路徑規劃完後, 用stroke()實際將線條路徑渲染出來.

moveTo(), lineTo()都是規劃路徑的工具, 前者為移動下筆座標點, 預設的下筆座標點為原點. 後者為從下筆點畫線到某個新的座標點.

順帶一提, <canvas>使用的是原點在左上, x軸向右為正, y軸向下為正的直角座標系.

照著上述程式碼, 只能不斷畫出從原點到座標位置的線而已. 雖然很漂亮, 但不是我們要的. 我們要的是一條會跟著畫筆軌跡移動的線.

一條任意形狀的線, 可以想成是非常多極小長度的直線所組成的. 這就是微分的概念, 也是我們下筆時能畫出任意線的概念.

// 初始化下筆點
let [lastX, lastY] = [0, 0];

function draw() {
  // ... 前略
	ctx.stroke();
	// 每次畫完後更新下筆點
	[lastX, lastY] = [e.offsetX, e.offsetY];
}

canvas.addEventListener('mousedown', (e) => {
	isDrawing = true;
  // 每次滑鼠按下時先將下筆點移到畫筆位置
	[lastX, lastY] = [e.offsetX, e.offsetY];
});

moveTo(lastX, lastY)的兩個參數代表下筆起始位置, 只要在每次下筆前先將起始位置移到畫筆位置, 每次畫畫就一定會由畫筆處開始.

由於draw()回呼函數是藉由監聽到mousemove而觸發的, 只要在畫完圖後將下筆位置更新成目前滑鼠所在位置, 下筆位置就會隨著滑鼠移動不斷更新, 按下滑鼠並移動就會畫出一堆極短但彼此連接的線, 總體看起來就是一條任意形狀的長線!

不過此時的線看起來不太圓滑, 我們可以在函數外初始化一些畫筆的設定, 讓線接起來圓滑一點.

ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.lineWidth = 20;

lineJoin是線跟線的接點處的樣式, lineCap是線的終點處的形狀, 設成round, 在線接合或終止時看起來比較順. lineWidth則控制了線的寬度, 一般建議設成偶數.

接下來我們希望讓畫出來的線條隨著移動而變色, 這次我們用hsl()來指定線條顏色. hsl()只是表示顏色的另一種方式, HSL分別代表'Hue'(顏色), 'Saturation(飽和度)', 'Lightness(亮度)'.

使用hsl()的好處在於, 第一個參數hue代表顏色, 範圍數值從0 ~ 360, 是循環的, 0 就是 360 就是紅色. 要讓畫筆顏色循環, 只需要將strokeStyle修正一下:

// 宣告並初始化hue
let hue = 0;

function draw() {
  // ...前略
  ctx.strokeStyle = `hsl(${hue}, 100%, 50%)`;
  
  // 讓hue隨著每次呼叫函式而累加
  hue++;
  if(hue >= 360) {hue = 0}
}

最後我們希望畫筆大小會隨著距離而變. 和hue的概念類似. 可以在hue下面接:

// direction決定畫筆大小的變化方向, 變大或變小
let direction = true;

function draw() {
  
  // ... 前略
  hue++;
  if(hue >= 360) {hue = 0}  

  // 隨著畫筆移動, 持續變大或變小
	if(direction) {
		ctx.lineWidth++;
	} else {
		ctx.lineWidth--;
	}
	
	// 超過特定範圍時修正畫筆大小的改變方向
	if(ctx.lineWidth >= 50 || ctx.lineWidth <= 10) {
		direction = !direction;
	}
}

direction控制畫筆變大或變小, 並隨著draw()被呼叫而改變逐漸往該方向變動尺寸.

到此任務完成!

以上就是JS30 第八天!

Reference

Canvas API 教學文件

練習的完整程式碼


上一篇
Day 7 - Array Cardio Part II
下一篇
Day 9 - Dev Tools
系列文
JS30 錄30

尚未有邦友留言

立即登入留言