canvas 基礎系列(一)之實現抽獎刮刮卡(橡皮擦)

木子七發表於2017-08-07

筆主最近一個多月以來 “深入“ 研究了 canvas 的實現原理,一口氣讀完了 《HTML5 Canvas 核心技術》這本書;而這一切以及這篇文章的誕生,都源於筆主公司的一位實習產品經理~
這位實習生擁有剛畢業時的血氣方剛,以及天馬行空的想象力;他從未考慮過專案的實際需求,以及上線時間成本,在我們公司以注重產品功能為導向的氛圍中,自成一派,成為一股清流 ?
一個推廣的小頁面,需要有山有水,有天空有流星,手指放上去還得泛起漣漪,拿到需求文件時嚇得我趕緊上 GG 搜 demo,這時我發現單個 demo 都能很好的執行在頁面中,可是當他們合併以後,在瀏覽器除錯皮膚中一行行鮮紅的文字不停的拍打著我的臉頰。我突然發現自己在遇到一些奇葩需求時,是多麼的無助。因為我不會 canvas 啊!
本系列文章旨在與大家分享筆主這段時間學習 canvas 的經驗,由淺入深,將自己實際工作過程中抄過的 demo (什麼?做個抽獎?評估時間?你等等,我搜下 demo…… 哦,不用評估時間了,已經改好了)都一一實現一遍,讓媽媽再也不用擔心我們遇到奇葩需求啦!

原文地址 喜歡就給我個大大的 ✨ 吧!

先上預覽 demo

scratchCard

我們假設我們的讀者都非常熟悉 javascript 原生語法。

本章涉及到的知識點

  • context 繪圖環境 我們可以通過 canvas 標籤來新建一個 canvas 畫布,這個 canvas 畫布使得我們可以在瀏覽器中操作畫布內的畫素,來實現繪製圖形,動畫等功能; 想要操作 canvas 畫布裡的內容,只能使用 javascript,千萬別嘗試用 css3 的動畫屬性去操作它,沒鳥用~

我們想要操作 canvas,首先需要獲取這個 canvas 物件

<canvas id="canvas" width="250" height="50"></canvas>
複製程式碼
    // 獲取 canvas 物件
let canvas = document.getElementById('canvas'),  
    // 獲取 cavans 繪圖環境物件,引數為 2d
	context = canvas.getContext('2d');
複製程式碼

context 是繪圖環境物件,如果你在瀏覽器中輸出 context,你會發現裡面包含了許多屬性,如:fillStyle strokeStyle等,以及一些函式方法,arc() rect() save() restore() clip()等等。


  • 繪製基本圖形

矩形

context.rect($x, $y, $width, $height)

矩形是最簡單的影像繪製了;

? 引數: xy:分別對應矩形左上角在 canvas 畫布中的座標; widthheight:就是矩形的寬高啦;

⚠️ 不過值得注意的是,呼叫這個方法,~只是繪製的矩形的路徑~,該路徑是不可見的,除非你在後面呼叫 context.fill()context.stroke() 方法,進行填充或者描邊。

填充描邊的表現形式取決於 context 繪圖環境的 fillStylestrokeStyle屬性的值。

圓形

context.arc($x, $y, $radius, $start_radian, $end_radian [, $clockwise])

圓形稍微複雜一丟丟,不過也很簡單啦。

? 引數: xy:代表圓的圓心在 canvas 畫布中的座標點; radius:圓的半徑,半徑,是半徑,不是直徑!start_radian $end_radian:起始角和終止角,他們接收的值只能是弧度。

記得圓周率麼?記得3.14是啥不,在 javascript 中 Math.PI 就代表的 π ;
簡單的說,圓有360°,而一個 π 是180°,你想畫個整圓,就是從 0° 到 360° 走一圈,Math.PI * 2;你想畫個半圓,就是 0° 到 180°,Math.PI

$clockwise:傳入布林值,false 圓由順時針繪製;true 圓由逆時針繪製。

和矩形相同,這個方法也只是繪製了一個路徑,想要在頁面顯示,任然是需要呼叫 context.fill()context.stroke() 兩個方法。

當前路徑

context.beginPath() 我們在建立一個集合圖形之前,都應當先呼叫該方法,該方法會清除上一次繪製時留下的路徑,並將本次繪製的路徑作為 ~當前路徑~。

繪製矩形和圓

let canvas = document.getElementById('canvas'),
    context = canvas.getContext('2d');

/**
 * 繪製一個矩形
 * @param {Num} x      矩形的左上角 x 軸的位置
 * @param {Num} y      矩形的左上角 y 軸的位置
 * @param {Num} width  矩形的寬度
 * @param {Num} height 矩形的高度
 */
function drawRect(x, y, width, height) {
	context.beginPath();               // 清空上一次的路徑
	context.rect(x, y, width, height); // 建立一個矩形路徑
	context.fill();                    // 將該矩形路徑填充為繪圖環境指定的填充顏色 context.fillStyle
}

/**
 * 繪製一個圓形
 * @param {Num} centerX  圓心 x 軸座標
 * @param {Num} centerY  圓心 y 軸座標
 * @param {Num} radius   圓的半徑
 */
function drawCircle(centerX, centerY, radius) {
    context.beginPath();
    context.arc(centerX, centerY, radius, 0, Math.PI * 2, false);
    context.fill();
}

drawRect(0, 0, 100, 100);
drawCircle(160, 50, 50);
複製程式碼

  • 清除畫布內容 context.clearRect($x, $y, $width, $height) 該方法,擦除規定矩形中的所有內容;引數同繪製矩形是一樣的,只不過這是清除!

  • canvas 的狀態

canvas 的狀態包括了:

  1. 當前座標變換資訊,transform 的東西,不用擔心,本章並不涉及;
  2. 剪輯區域,這是本章的核心,context.clip() 後面會詳細講到;
  3. 所有繪圖環境(context)屬性,也就剛剛提到的,fillStyle[規定填充的顏色] strokeStyle[規定描邊的顏色],這些狀態決定的 canvas 中繪製的影像的展示效果,他們是全域性的,也就是說一旦規定了fillStyle為紅色,那麼除非在 javascript 後面重置這個屬性,否則你將來繪製出來的幾何圖形永遠都是紅色的。
context.fillStyle = 'red';
drawRect(0, 0, 100, 100);   // -> red
drawRect(110, 0, 100, 100); // -> red

context.fillStyle = 'blue';
drawRect(220, 0, 100, 100); // -> blue
複製程式碼

  • canvas 的狀態儲存與恢復 前面已經提到過,canvas 的狀態是什麼。 而這裡引出的是繪圖環境中的兩個很重要的方法,save() 和 restore()。

如果有個需求,我想要先繪製一個紅色的矩形,接著繪製一個藍色的矩形,最後再繪製一個紅色的矩形

我們知道 javascript 是逐條同步執行的,那麼要實現上面的需求,我們需要先定義繪圖環境的填充顏色為紅色,繪製了第一個紅色矩形後,再定義繪圖環境的填充顏色為藍色,繪製,再定義繪圖環境的填充顏色為紅色,就像這樣:

context.fillStyle = 'red';
drawRect(0, 0, 100, 100);   // -> red

context.fillStyle = 'blue';
drawRect(110, 0, 100, 100); // -> blue

context.fillStyle = 'red';
drawRect(220, 0, 100, 100); // -> red
複製程式碼

是不是覺得這樣做非常的傻~

此時 save() 和 restore() 就站出來說不了!

context.save():能將執行該方法之前的所有 ~canvas 狀態~ 儲存下來; context.restore() :將 save 方法儲存的 ~canvas 狀態~ 釋放出來;

所以上面弱智的寫法,我們可以寫成這樣:

context.fillStyle = 'red';
drawRect(0, 0, 100, 100);    // - red 

context.save();

context.fillStyle = 'blue';
drawRect(110, 0, 100, 100);  // -> blue

context.restore();
drawRect(220, 0, 100, 100);  // -> red
複製程式碼

save 方法儲存的是 context.fillStyle = 'red' 這個狀態,當設定這個狀態為藍色時,context 繪圖環境的 fillStyle 屬性變成了藍色,當執行 restore 方法時,又被恢復成了紅色。


  • 剪輯區域 clip()

context 繪圖環境物件提供了一個 context.clip() 剪輯區域方法,這也是本章實現橡皮擦功能的核心方法。

該方法會將 ~當前路徑~ 作為一個剪輯區域,使該區域以外的影像不可見。

並且在剪輯區域中,所有填充,清除等操作不會影響剪輯區域以外的內容。

所以我們可以建立一個剪輯區域,在這個剪輯區域中執行清除操作,就可以擦除 canvas 畫布中指定的內容。

drawRect(0, 0, 100, 100);

context.save();

// 建立一個圓形路徑,並作為剪輯區域,擦除該區域中的所有內容
drawCircle(0, 0, 50);
context.clip();
context.clearRect(0, 0, canvas.width, canvas.height);

context.restore();

drawRect(110, 0, 100, 100);
複製程式碼

開始實現橡皮擦

本章需要用到的基礎知識就講到這裡啦,如果大家還有疑問,可以查閱 w3school 上的 canvas 文件

⚠️ 這個 demo 只能執行在手機上,瀏覽器需要開啟手機模擬。

? 思路:

  1. 定義一個公用繪製剪輯區域的方法 drawEraser() ,該方法會將當前的路徑作為剪輯路徑繪製,並::清除該路徑內的所有內容::;
  2. 手指按下時,觸發 touchstart 事件,開啟 dragging 狀態,呼叫 drawEraser() 方法,繪製第一個剪輯區域;
  3. 手指移動過程中,不斷的呼叫 drawEraser() 方法,繪製剪輯區域,實現橡皮擦效果。
<!DOCTYPE html>
<html>

<head>
	<title>橡皮擦</title>
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
</head>

<body>

	<canvas id="canvas" width="300" height="300" style="background: #eee">
		Canvas not supported
	</canvas>

	<script>
		var canvas = document.getElementById('canvas'),
			context = canvas.getContext('2d'),

			ERASER_SIZE = 15,  // 橡皮擦的大小

			dragging = false;  // 是否處在拖動狀態

		/**
		 * 轉換座標值
		 * 將滑鼠點選或移動時獲取的座標值,減去 canvas 相對視窗的座標值,就是在 canvas 畫布中的座標值
		 * @param {Obj} e 手指當前相對視窗的座標位置
		 */
		function windowToCanvas(e) {
			let x = e.targetTouches[0].clientX,
				y = e.targetTouches[0].clientY,
				bbox = canvas.getBoundingClientRect();

			return {
				x: x - bbox.left,
				y: y - bbox.top
			}
		}

		/**
		 * 繪製剪輯區域,並清除該區域中的內容
		 * @param {Obj} loc 手指當前相對 canvas 畫布中的座標位置
		 */
		function drawEraser(loc) {
			context.save();
			context.beginPath();
			context.arc(loc.x, loc.y, ERASER_SIZE, 0, Math.PI * 2, false);
			context.clip();
			context.clearRect(0, 0, canvas.width, canvas.height);
			context.restore();
		}

		/**
		 * 頁面載入時,繪製一個鋪滿 canvas 畫布的矩形
		 * 該矩形用於被擦除
		 */
		window.onload = function (e) {
			context.save();
			context.fillStyle = '#666';
			context.beginPath();
			context.fillRect(0, 0, canvas.width, canvas.height);
			context.restore();
		}

		/**
		 * ① 手指按下時,開啟 dragging 狀態,並繪製剪輯區域
		 */
		canvas.addEventListener('touchstart', function (e) {
			var loc = windowToCanvas(e);
			dragging = true;
			drawEraser(loc);
		})

		/**
		 * ② 手指移動時,不斷進行剪輯區域的繪製,以及路徑更新,實現擦除的效果
		 */
		canvas.addEventListener('touchmove', function (e) {
			var loc;

			if (dragging) {
				loc = windowToCanvas(e);
				drawEraser(loc);
			}
		})

		/**
		 * ③ 手指離開,結束擦除過程
		 */
		canvas.addEventListener('touchend', function (e) {
			dragging = false;
		})
	</script>
</body>

</html>
複製程式碼

結語

  • 使用 canvas 實現橡皮擦功能是非常簡單的。我們可以把橡皮擦的形狀換成矩形,多邊形,五角星等等;只需要改變繪製剪輯區域的路徑就行了;

  • 刮刮卡的功能,也完全是由橡皮擦功能實現的,我們可以把 canvas 的背景圖片設定為開獎的內容,文字等,用一個陣列將這些圖片的路徑包含進去,每次載入頁面時,隨機呼叫一個陣列元素;

  • 這期內容就講到這裡,下期帶大家用 canvas 實現 大轉盤抽獎,九宮格抽獎。3Q,阿里呀多。

原文地址 喜歡就給我個大大的 ✨ 吧!

相關文章