筆主最近一個多月以來 “深入“ 研究了 canvas 的實現原理,一口氣讀完了 《HTML5 Canvas 核心技術》這本書;而這一切以及這篇文章的誕生,都源於筆主公司的一位實習產品經理~
這位實習生擁有剛畢業時的血氣方剛,以及天馬行空的想象力;他從未考慮過專案的實際需求,以及上線時間成本,在我們公司以注重產品功能為導向的氛圍中,自成一派,成為一股清流 ?
一個推廣的小頁面,需要有山有水,有天空有流星,手指放上去還得泛起漣漪,拿到需求文件時嚇得我趕緊上 GG 搜 demo,這時我發現單個 demo 都能很好的執行在頁面中,可是當他們合併以後,在瀏覽器除錯皮膚中一行行鮮紅的文字不停的拍打著我的臉頰。我突然發現自己在遇到一些奇葩需求時,是多麼的無助。因為我不會 canvas 啊!
本系列文章旨在與大家分享筆主這段時間學習 canvas 的經驗,由淺入深,將自己實際工作過程中抄過的 demo (什麼?做個抽獎?評估時間?你等等,我搜下 demo…… 哦,不用評估時間了,已經改好了)都一一實現一遍,讓媽媽再也不用擔心我們遇到奇葩需求啦!
原文地址 喜歡就給我個大大的 ✨ 吧!
先上預覽 demo
我們假設我們的讀者都非常熟悉 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)
矩形是最簡單的影象繪製了;
? 引數:
$x $y:分別對應矩形左上角在 canvas 畫布中的座標;
$width $height:就是矩形的寬高啦;
⚠️ 不過值得注意的是,呼叫這個方法,~只是繪製的矩形的路徑~,該路徑是不可見的,除非你在後面呼叫 context.fill()
和 context.stroke()
方法,進行填充或者描邊。
填充描邊的表現形式取決於 context 繪圖環境的 fillStyle
和strokeStyle
屬性的值。
圓形
context.arc($x, $y, $radius, $start_radian, $end_radian [, $clockwise])
圓形稍微複雜一丟丟,不過也很簡單啦。
? 引數:
$x $y:代表圓的圓心在 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 的狀態包括了:
- 當前座標變換資訊,
transform
的東西,不用擔心,本章並不涉及; - 剪輯區域,這是本章的核心,
context.clip()
後面會詳細講到; - 所有繪圖環境(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 只能執行在手機上,瀏覽器需要開啟手機模擬。
? 思路:
- 定義一個公用繪製剪輯區域的方法
drawEraser()
,該方法會將當前的路徑作為剪輯路徑繪製,並::清除該路徑內的所有內容::; - 手指按下時,觸發 touchstart 事件,開啟 dragging 狀態,呼叫
drawEraser()
方法,繪製第一個剪輯區域; - 手指移動過程中,不斷的呼叫
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,阿里呀多。
原文地址 喜歡就給我個大大的 ✨ 吧!