刮刮卡是你們很是熟悉的一種網頁交互元素了。實現刮塗層的效果,須要藉助canvas來實現,想必每一個前端工程師都清楚。實現刮刮卡並不難,但其中卻涉及不少知識點,掌握這些知識點,有助於咱們更深入理解原理,對於提高觸類旁通的能力頗有幫助。本期以實現刮刮卡爲例,分享下如何科學合理地封裝函數,並對涉及的相關知識點進行講解。html
先看下最終效果:前端
實現刮刮卡都涉及到哪些知識點呢?git
知識點1:canvas元素尺寸與畫布尺寸github
知識點2:prototype、__proto__、constructorchrome
知識點3:canvas的globalCompositeOperationcanvas
知識點4:addEventListener第三個參數的passive屬性數組
知識點5:canvas的ImageData瀏覽器
下面進入本期分享的正式內容。安全
爲了知足更多的場景須要,咱們儘量地提供更多的參數,方便使用者。先從產品和UI的角度來思考下,一個刮刮卡可能須要哪些配置選項。bash
接下來再補充下技術配置選項:
OK,確認好以上配置參數後,就能夠正式開工了。
項目目錄結構以下:
|- award.jpg <--刮刮卡底層結果頁圖片
|- index.html
|- scratch-2x.jpg <--刮刮卡塗層圖片
|- scratchcard.js
複製代碼
頁面結構很簡單,div的background顯示結果,div裏的canvas用來作塗層。
新建index.html,加入如下代碼(HTML模板代碼略過):
HTML代碼:
<div class="card">
<canvas id="canvas" width="750" height="280"></canvas>
</div>
複製代碼
CSS代碼:
.card {
width: 375px;
height: 140px;
background: url('award.jpg');
background-size: 375px 140px;
}
.card canvas {
width: 375px;
height: 140px;
}
複製代碼
award.jpg用的是2倍圖,所以使用 background-size縮放回1倍顯示大小。
這裏能夠發現,HTML中canvas的width、height與CSS中的width、height不一致。緣由就是要適應Retina 2倍屏幕。這裏就涉及到了canvas畫布尺寸的知識點。
如今頁面顯示效果以下,結果圖像已顯示出來:
HTML中canvas的width、height是畫布大小,通俗來說就是canvas畫布的「繪製區域大小」,必定要跟元素的顯示大小區別開來。
咱們的結果圖素材是750x280,因此要讓canvas徹底繪製這張圖片,畫布大小也須要是750x280。
那麼元素大小,就是canvas在頁面的「顯示大小」。經過CSS對canvas元素進行寬高設置,使其正確的顯示。
新建scratchcard.js。
結合第1章節的需求分析,類的雛形以下:
function ScratchCard(config) {
// 默認配置
this.config = {
// canvas元素
canvas: null,
// 直接所有刮開的百分比
showAllPercent: 65,
// 圖片圖層
coverImg: null,
// 純色圖層,若是圖片圖層值不爲null,則純色圖層無效
coverColor: null,
// 所有刮開回調
doneCallback: null,
// 擦除半徑
radius: 20,
// 屏幕倍數
pixelRatio: 1,
// 展示所有的淡出效果時間(ms)
fadeOut: 2000
}
Object.assign(this.config, config);
}
複製代碼
使用對象的方式向函數傳參有不少優勢:
使用Object.assign方法,可將傳遞進來的config參數覆蓋默認參數。傳遞的config中沒有的屬性,則使用默認配置。
在index.html中引入scratchcard.js,在body最下邊插入script代碼:
new ScratchCard({
canvas: document.getElementById('canvas'),
coverImg: 'scratch-2x.jpg',
pixelRatio: 2,
doneCallback: function() {
console.log('done')
}
});
複製代碼
刮刮卡的類使用起來很是方便,僅傳遞不使用默認配置的值便可。
繼續編寫scratchcard.js:
function ScratchCard(config) {
this.config = {
...(略)
}
Object.assign(this.config, config)
+ this._init();
}
+ ScratchCard.prototype = {
+ constructor: ScratchCard,
+ // 初始化
+ _init: function() {}
+ }
複製代碼
這裏設置了constructor: ScratchCard,僅僅是爲了顯得更加嚴謹,省略這一行也是沒有問題的。
由代碼中prototype
和constructor
引出第2個知識點。
先記住兩點:
__proto__
和constructor
屬性是對象所獨有的(函數也是對象)。prototype
屬性是函數所獨有的。※因爲JS中函數也是一種對象,因此函數也擁有
__proto__
和constructor
屬性。
【__proto__】
__proto__
屬性都是由一個對象指向一個對象,即指向它們的原型對象(也能夠理解爲父對象)。
它的做用就是當訪問一個對象的屬性時,若是該對象內部不存在這個屬性,那麼就會去它的__proto__
屬性所指向的那個對象(父對象)裏找,若是父對象也不存在這個屬性,則繼續在父對象的__proto__
屬性所指向的對象(爺爺對象)裏找,若是還沒找到,則繼續往上找,直到原型鏈頂端null。null爲原型鏈的終點。
由以上這種經過__proto__
屬性來鏈接對象直到null的一條鏈即爲所謂的原型鏈。
【prototype】
prototype
對象是函數所獨有的,它是從一個函數指向一個對象。它的含義是函數的原型對象,也就是由這個函數所建立的實例的原型對象。
// 示例代碼
var demo = new Demo()
function Demo(config) { ... }
複製代碼
所以,以上代碼中,demo.__proto__ === Demo.prototype。
prototype
屬性的做用就是:prototype
包含的屬性和方法可被其建立的所有實例所共用。
【constructor】
constructor
屬性也是對象獨有的,它是從一個對象指向一個函數。其含義就是指向該對象的構造函數。全部函數最終的構造函數都指向Function。
當建立一個函數的時候,會同時自動建立它的prototype
對象,這個對象也會自動得到constructor
屬性,並指向本身。
那麼,爲何咱們這裏還要手動設置constructor: ScratchCard呢?
緣由就是咱們用這樣的語法:
ScratchCard.prototype = {}
複製代碼
會致使自動設置的constructor屬性值被覆蓋。在這種狀況下,若是咱們不特地設置constructor: ScratchCard的話,constructor則會指向Object。
先添加如下代碼:
function ScratchCard(config) {
this.config = {
...(略)
}
Object.assign(this.config, config);
+ this.canvas = this.config.canvas;
+ this.ctx = null;
+ this.offsetX = null;
+ this.offsetY = null;
this._init();
}
ScratchCard.prototype = {
constructor: ScratchCard,
// 初始化
_init: function() {
+ var that = this;
+ this.ctx = this.canvas.getContext('2d');
+ this.offsetX = this.canvas.offsetLeft;
+ this.offsetY = this.canvas.offsetTop;
+ if (this.config.coverImg) {
+ // 若是設置的圖片塗層
+ var coverImg = new Image();
+ coverImg.src = this.config.coverImg;
+ // 讀取圖像
+ coverImg.onload = function() {
+ // 繪製圖像
+ that.ctx.drawImage(coverImg, 0, 0);
+ that.ctx.globalCompositeOperation = 'destination-out';
+ }
+ } else {
+ // 若是沒設置圖片塗層,則使用純色塗層
+ this.ctx.fillStyle = this.config.coverColor;
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+ this.ctx.globalCompositeOperation = 'destination-out';
+ }
}
}
複製代碼
初始化代碼就是實現塗層的覆蓋。這裏的關鍵邏輯是:若是設置了圖像塗層,則忽略純色塗層。
涉及到了canvas兩個API:
drawImage
用於繪製圖像。
fillRect
用於繪製矩形,在繪製以前要先設置筆刷,即經過fillStyle
屬性設置顏色。
這段代碼是什麼意思呢?
this.ctx.globalCompositeOperation = 'destination-out';
複製代碼
globalCompositeOperation就是第3個知識點。
在w3school上能夠查閱到該屬性的詳細說明:
值 | 描述 |
---|---|
source-over | 默認。在目標圖像上顯示源圖像。 |
source-atop | 在目標圖像頂部顯示源圖像。源圖像位於目標圖像以外的部分是不可見的。 |
source-in | 在目標圖像中顯示源圖像。只有目標圖像內的源圖像部分會顯示,目標圖像是透明的。 |
source-out | 在目標圖像以外顯示源圖像。只會顯示目標圖像以外源圖像部分,目標圖像是透明的。 |
destination-over | 在源圖像上方顯示目標圖像。 |
destination-atop | 在源圖像頂部顯示目標圖像。源圖像以外的目標圖像部分不會被顯示。 |
destination-in | 在源圖像中顯示目標圖像。只有源圖像內的目標圖像部分會被顯示,源圖像是透明的。 |
destination-out | 在源圖像外顯示目標圖像。只有源圖像外的目標圖像部分會被顯示,源圖像是透明的。 |
lighter | 顯示源圖像 + 目標圖像。 |
copy | 顯示源圖像。忽略目標圖像。 |
xor | 使用異或操做對源圖像與目標圖像進行組合。 |
看上去好像有點懵逼難理解,其實就是相似於指定photoshop裏兩個圖層怎麼融合,好比誰遮罩誰、交叉部分消除、交叉部分顏色融合等等。
能夠參看下w3school的圖示,藍色爲目標圖像,紅色爲源圖像。
回到刮刮卡,圖片塗層是目標圖像,目前源圖像還未設置,因此源圖像爲全透明(源圖像的不透明的部分用來摳除目標圖像並呈現透明),因此目標圖像(圖片塗層)所有顯示。
如今效果以下圖所示,塗層已經覆蓋上了。
塗抹事件,其實就是用touchstart、touchmove、touchend事件,爲了順便兼容鼠標操做,也把mousedown、mousemove、mouseup帶上。
修改代碼:
function ScratchCard(config) {
this.config = {
...(略)
}
Object.assign(this.config, config);
this.canvas = this.config.canvas;
this.ctx = null;
this.offsetX = null;
this.offsetY = null;
+ // 是否在畫布上處於按下狀態
+ this.isDown = false;
+ // 是否已完成刮刮卡
+ this.done = false;
this._init();
}
ScratchCard.prototype = {
constructor: ScratchCard,
// 初始化
_init: function() {
...(略)
this.offsetY = this.canvas.offsetTop;
+ this._addEvent();
if (this.config.coverImg) { ...(略) }
},
+ // 添加事件
+ _addEvent: function() {
+ this.canvas.addEventListener('touchstart', this._eventDown.bind(this), { passive: false });
+ this.canvas.addEventListener('touchend', this._eventUp.bind(this), { passive: false });
+ this.canvas.addEventListener('touchmove', this._scratch.bind(this), { passive: false });
+ this.canvas.addEventListener('mousedown', this._eventDown.bind(this), { passive: false });
+ this.canvas.addEventListener('mouseup', this._eventUp.bind(this), { passive: false });
+ this.canvas.addEventListener('mousemove', this._scratch.bind(this), { passive: false });
+ },
+ _eventDown: function(e) {
+ e.preventDefault();
+ this.isDown = true;
+ },
+ _eventUp: function(e) {
+ e.preventDefault();
+ this.isDown = false;
+ },
+ // 刮塗層
+ _scratch: function(e) {
+ }
}
複製代碼
代碼很好理解,就是添加事件監聽。當按下的時候,把isDown設置爲true,當擡起的時候,把isDown設置爲false。
能夠看到addEventListener的第3個參數{ passive: false },這是個什麼鬼?這就是第4個知識點。
最開始,addEventListener() 的參數約定是這樣的:
el.addEventListener(type, listener, useCapture)
複製代碼
2015年末,爲了擴展新的選項,DOM 規範作了修訂:
el.addEventListener(type, listener, {
capture: false, // useCapture
once: false, // 是否設置單次監聽
passive: false // 是否讓阻止默認行爲preventDefault()失效
})
複製代碼
三個屬性的默認值都爲 false。
爲何會多出個passive屬性呢?
爲了防止頁面滾動,不少移動端頁面都會監聽 touchmove 等 touch 事件,像這樣:
document.addEventListener("touchmove", function(e){
e.preventDefault()
})
複製代碼
因爲 touchmove 事件對象的 cancelable 屬性爲 true,也就是說它的默認行爲能夠被監聽器經過 preventDefault() 方法阻止。那它的默認行爲是什麼呢,一般來講就是滾動當前頁面(還多是縮放頁面),若是它的默認行爲被阻止了,頁面就必須靜止不動。但瀏覽器沒法預先知道一個監聽器會不會調用 preventDefault(),它能作的只有等監聽器執行完後再去執行默認行爲,而監聽器執行是要耗時的,有些甚至耗時很明顯,這樣就會致使頁面卡頓。即使監聽器是個空函數,也會產生必定的卡頓,畢竟空函數的執行也會耗時。
當設置了passtive爲true,則會忽略代碼中的preventDefault(), 所以頁面會變得更流暢。以下演示,右側手機的頁面設置了passtive爲true。
OK,那麼問題來了?既然默認是passive: false,爲何代碼裏還要再畫蛇添足寫一遍呢?
答案在這裏,來看chrome的官方說明: www.chromestatus.com/feature/509…
原文以下:
AddEventListenerOptions defaults passive to false. With this change touchstart and touchmove listeners added to the document will default to passive:true (so that calls to preventDefault will be ignored)..
意思是:addEventListener的option裏,默認passive是false。可是若是事件是 touchstart 或 touchmove的話,passive的默認值則會變成true(因此preventDefault就會被忽略了)。
OK,原理講完了,咱們尚未把頁面的默認滑動行爲阻止掉。不阻止的話,在滑動刮刮卡的時候,頁面也會跟着滾動。
看完了4.3小節,那麼阻止頁面滾動就很簡單了。在index.html的script里加入如下代碼:
+ window.addEventListener('touchmove', function(e) {
+ e.preventDefault();
+ }, {passive: false});
new ScratchCard({
...(略)
});
複製代碼
這裏完善下_scratch方法,代碼以下:
_scratch: function(e) {
e.preventDefault();
var that = this;
if (!this.done && this.isDown) {
if (e.changedTouches) {
e = e.changedTouches[e.changedTouches.length - 1];
}
var x = (e.clientX + document.body.scrollLeft || e.pageX) - this.offsetX || 0,
y = (e.clientY + document.body.scrollTop || e.pageY) - this.offsetY || 0;
with(this.ctx) {
beginPath()
arc(x * that.config.pixelRatio, y * that.config.pixelRatio, that.config.radius * that.config.pixelRatio, 0, Math.PI * 2);
fill();
}
}
}
複製代碼
邏輯大體以下:
須要說明的是,乘以pixelRatio是爲了適應多倍屏幕。在本示例中,畫布尺寸是2倍尺寸,而座標是按照網頁元素的尺寸計算出來的,正好相差一倍,因此要乘以pixelRatio(pixelRatio = 2)。
還記得4.2小節講的globalCompositeOperation麼?當設置爲destination-out
的時候,源圖像的非透明部分會摳去目標圖像,所以實現了刮刮卡的刮塗層效果。
雖然刮塗層的效果實現了,可是還要實時檢測刮開了多少,來判斷是否完成刮刮卡。
繼續修改代碼:
_scratch: function(e) {
...(略)
if (!this.done && this.isDown) {
...(略)
with(this.ctx) {
...(略)
}
+ if (this._getFilledPercentage() > this.config.showAllPercent) {
+ this._scratchAll()
+ }
}
}
+ // 刮開所有塗層
+ _scratchAll() {
+ var that = this;
+ this.done = true;
+ if (this.config.fadeOut > 0) {
+ // 先使用CSS opacity清除,再使用canvas清除
+ this.canvas.style.transition = 'all ' + this.config.fadeOut / 1000 + 's linear';
+ this.canvas.style.opacity = '0';
+ setTimeout(function() {
+ that._clear();
+ }, this.config.fadeOut)
+ } else {
+ // 直接使用canvas清除
+ that._clear();
+ }
+ // 執行回調函數
+ this.config.doneCallback && this.config.doneCallback();
+ },
+ // 清除所有塗層
+ _clear() {
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+ },
+ // 獲取刮開區域百分比
+ _getFilledPercentage: function() {
+ var imgData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
+ // 存儲當前cavnas畫布的所有像素點信息
+ var pixels = imgData.data;
+ // 存儲當前canvas畫布的透明像素信息
+ var transPixels = [];
+ // 遍歷所有像素點信息
+ for (var i = 0; i < pixels.length; i += 4) {
+ // 把透明的像素點添加到transPixels裏
+ if (pixels[i + 3] < 128) {
+ transPixels.push(pixels[i + 3]);
+ }
+ }
+ // 計算透明像素點的佔比
+ return (transPixels.length / (pixels.length / 4) * 100).toFixed(2)
+ }
}
複製代碼
新增了3個方法:
_scratchAll
: 清空塗層(所有刮開)。若是設置的fadeOut(淡出時間),則經過CSS動畫,將canvas作淡出效果,而後再清除塗層。若是fadeOut爲0,則直接清除塗層。
_clear
:清除塗層。很簡單,直接畫一個鋪滿畫布的矩形便可。
_getFilledPercentage
:計算刮開區域的百分比。經過遍歷canvas每一個像素點,計算全透明像素的佔比。
這裏就涉及到了第5個知識點。
利用canvas的getImageData()方法能夠獲取到所有的像素點信息,返回數組格式。數組中,並非每一個元素表明一個像素的信息,而是每4個元素爲一個像素的信息。例如:
data[0] = 像素1的R值,紅色(0-255)
data[1] = 像素1的G值,綠色(0-255)
data[2] = 像素1的B值,藍色(0-255)
data[3] = 像素1的A值,alpha 通道(0-255; 0 透明,255徹底可見)
data[4] = 像素2的R值,紅色(0-255)
...
本例的透明度不存在中間值,因此就能夠認爲alpha小於128即爲透明。
因爲瀏覽器安全限制,Image不能讀取本地圖片,所以須要部署在服務端,以http協議瀏覽本項目。
以上就是本期分享的所有內容了。完整代碼請前往GitHub:github.com/Yuezi32/scr…
看似簡單的刮刮卡卻隱藏了這麼多的知識點,你都掌握了麼?
歡迎關注個人我的微信公衆號,隨時獲取最新文章^_^