一張刮刮卡竟包含這麼多前端知識點

刮刮卡是你們很是熟悉的一種網頁交互元素了。實現刮塗層的效果,須要藉助canvas來實現,想必每一個前端工程師都清楚。實現刮刮卡並不難,但其中卻涉及不少知識點,掌握這些知識點,有助於咱們更深入理解原理,對於提高觸類旁通的能力頗有幫助。本期以實現刮刮卡爲例,分享下如何科學合理地封裝函數,並對涉及的相關知識點進行講解。html

先看下最終效果:前端

實現刮刮卡都涉及到哪些知識點呢?git

知識點1:canvas元素尺寸與畫布尺寸github

知識點2:prototype、__proto__、constructorchrome

知識點3:canvas的globalCompositeOperationcanvas

知識點4:addEventListener第三個參數的passive屬性數組

知識點5:canvas的ImageData瀏覽器

下面進入本期分享的正式內容。安全

1 需求分析

爲了知足更多的場景須要,咱們儘量地提供更多的參數,方便使用者。先從產品和UI的角度來思考下,一個刮刮卡可能須要哪些配置選項。bash

  • 塗層樣式(圖片 or 純色)
  • 塗抹畫筆半徑
  • 塗抹到百分之多少時,直接刮開所有塗層
  • 刮開所有塗層的效果(淡出 or 直接消除)

接下來再補充下技術配置選項:

  • canvas元素
  • 屏幕像素顯示倍數(適應Retina等高倍屏)
  • 淡出效果的過渡動畫時間

OK,確認好以上配置參數後,就能夠正式開工了。

2 頁面構建

項目目錄結構以下:

|- 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畫布尺寸的知識點。

如今頁面顯示效果以下,結果圖像已顯示出來:

知識點1:canvas元素尺寸與畫布尺寸

HTML中canvas的width、height是畫布大小,通俗來說就是canvas畫布的「繪製區域大小」,必定要跟元素的顯示大小區別開來。

咱們的結果圖素材是750x280,因此要讓canvas徹底繪製這張圖片,畫布大小也須要是750x280。

那麼元素大小,就是canvas在頁面的「顯示大小」。經過CSS對canvas元素進行寬高設置,使其正確的顯示。

3 構建類的雛形

新建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);
}
複製代碼

使用對象的方式向函數傳參有不少優勢:

  1. 參數語義化,方便理解
  2. 不用在乎參數順序
  3. 傳參的增刪和順序調整不會影響業務代碼的使用

使用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')
    }
});
複製代碼

刮刮卡的類使用起來很是方便,僅傳遞不使用默認配置的值便可。

4 實現ScratchCard

4.1 構建ScratchCard原型

繼續編寫scratchcard.js:

function ScratchCard(config) {
        this.config = {
            ...(略)
        }
        Object.assign(this.config, config)
+       this._init();
    }

+   ScratchCard.prototype = {
+       constructor: ScratchCard,
+       // 初始化
+       _init: function() {}
+   }
複製代碼

這裏設置了constructor: ScratchCard,僅僅是爲了顯得更加嚴謹,省略這一行也是沒有問題的。

由代碼中prototypeconstructor引出第2個知識點。

知識點2:prototype、__proto__、constructor

先記住兩點:

  1. __proto__constructor屬性是對象所獨有的(函數也是對象)。
  2. 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。

4.2 實現canvas塗層

先添加如下代碼:

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個知識點。

知識點3:canvas的globalCompositeOperation

在w3school上能夠查閱到該屬性的詳細說明:

描述
source-over 默認。在目標圖像上顯示源圖像。
source-atop 在目標圖像頂部顯示源圖像。源圖像位於目標圖像以外的部分是不可見的。
source-in 在目標圖像中顯示源圖像。只有目標圖像內的源圖像部分會顯示,目標圖像是透明的。
source-out 在目標圖像以外顯示源圖像。只會顯示目標圖像以外源圖像部分,目標圖像是透明的。
destination-over 在源圖像上方顯示目標圖像。
destination-atop 在源圖像頂部顯示目標圖像。源圖像以外的目標圖像部分不會被顯示。
destination-in 在源圖像中顯示目標圖像。只有源圖像內的目標圖像部分會被顯示,源圖像是透明的。
destination-out 在源圖像外顯示目標圖像。只有源圖像外的目標圖像部分會被顯示,源圖像是透明的。
lighter 顯示源圖像 + 目標圖像。
copy 顯示源圖像。忽略目標圖像。
xor 使用異或操做對源圖像與目標圖像進行組合。

看上去好像有點懵逼難理解,其實就是相似於指定photoshop裏兩個圖層怎麼融合,好比誰遮罩誰、交叉部分消除、交叉部分顏色融合等等。

能夠參看下w3school的圖示,藍色爲目標圖像,紅色爲源圖像。

回到刮刮卡,圖片塗層是目標圖像,目前源圖像還未設置,因此源圖像爲全透明(源圖像的不透明的部分用來摳除目標圖像並呈現透明),因此目標圖像(圖片塗層)所有顯示。

如今效果以下圖所示,塗層已經覆蓋上了。

4.3 添加塗抹事件

塗抹事件,其實就是用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個知識點。

知識點4:addEventListener第三個參數的passive屬性

最開始,addEventListener() 的參數約定是這樣的:

el.addEventListener(type, listener, useCapture)
複製代碼
  • el:事件對象
  • type:事件類型,click、mouseover 等
  • listener:事件處理函數,也就是事件觸發後的回調
  • useCapture:布爾值,是不是捕獲型,默認 false(冒泡)

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.4 阻止頁面滾動

看完了4.3小節,那麼阻止頁面滾動就很簡單了。在index.html的script里加入如下代碼:

+   window.addEventListener('touchmove', function(e) {
+       e.preventDefault();
+   }, {passive: false});

    new ScratchCard({
        ...(略)
    });
複製代碼

4.5 實現擦除效果

這裏完善下_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();
        }
    }
}
複製代碼

邏輯大體以下:

  1. 判斷刮刮卡還沒刮完(this.done爲false),而且處於按下狀態(this.isDown爲true)。
  2. 若是存在多個觸點,則使用最後一個觸點。經過e.changedTouches獲取最後一個觸點。
  3. 計算觸點在canvas裏的相對座標。
  4. 在canvas中的觸點位置繪製圓形。

須要說明的是,乘以pixelRatio是爲了適應多倍屏幕。在本示例中,畫布尺寸是2倍尺寸,而座標是按照網頁元素的尺寸計算出來的,正好相差一倍,因此要乘以pixelRatio(pixelRatio = 2)。

還記得4.2小節講的globalCompositeOperation麼?當設置爲destination-out的時候,源圖像的非透明部分會摳去目標圖像,所以實現了刮刮卡的刮塗層效果。

4.6 檢測塗層的透明部分佔比

雖然刮塗層的效果實現了,可是還要實時檢測刮開了多少,來判斷是否完成刮刮卡。

繼續修改代碼:

_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個知識點。

知識點5:canvas的ImageData

利用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即爲透明。

4.7 注意事項

因爲瀏覽器安全限制,Image不能讀取本地圖片,所以須要部署在服務端,以http協議瀏覽本項目。

以上就是本期分享的所有內容了。完整代碼請前往GitHub:github.com/Yuezi32/scr…

看似簡單的刮刮卡卻隱藏了這麼多的知識點,你都掌握了麼?

歡迎關注個人我的微信公衆號,隨時獲取最新文章^_^

相關文章
相關標籤/搜索