網頁上也能做刮刮樂!運用 HTML5 canvas 合成功能
我就先不賣關子,底下這個 fiddle 就是這篇文章的刮刮樂範例完整版。
可以玩玩看,試著瞭解裡面的程式腳本,或是 fork 來改改看。
有興趣但看不太懂 code 的話, 就跟著本篇文章的介紹帶你入門吧!
如果大家有看過九月份的「Firefox OS 讓你儘情享受每一刻」活動網頁(註 1),
應該很好奇一開始的刮刮樂是如何做到的吧?
在研究該如何實作這個功能的時候,因為刮刮樂和繪圖的概念很類似,
所以第一直覺就是想到要用 HTML5 Canvas。
但 Canvas 如此博大精深,究竟要從何做起呢?讓我們繼續看下去……
瞭解 Canvas 的合成參數設定
Canvas
的 globalCompositeOperation
屬性設定可以打破原本圖形只能依繪製順序往上覆蓋的限制,它可以將圖形畫在另一圖形之下,也可用來遮蔽、清除圖形區域。讓圖形的繪製組合更有彈性。在 MDN 的 Canvas 合成效果教學文件(註 2)裡可以看到各種不同合成參數的執行效果:
(注意其中 darker 已從 canvas 標準規範中移除並不再支援)
圖例中藍色是設定合成參數之前繪製上去的,也就是 destination;
紅色則是設定合成參數之後繪製上去的,也就是 source。
哇靠 12 種也太多了吧!簡直是大海撈針!
但冷靜下來仔細想想,其實刮刮樂不就和小畫家的橡皮擦功能差不多嗎?
如果藍色方塊是我想要擦掉的圖形,
而紅色的圓圈是橡皮擦工具,刮完後剩下的不就是一個缺圓角的方塊了嗎?
因此答案很明顯了:登登登- destiniation-out
就是你啦!
用滑鼠刮掉圖案
要開始寫一個功能時,第一件要做的事絕對不是建立一個空白的 javascript 檔案,重新發明輪子,
網路上充滿了熱心的神人,你想得到的功能幾乎早就有人做出來並公開原始碼。
我們只需要想出對的關鍵字,去把它找出來就行了。
於是我在谷歌輸入「javascript scratcher」,運氣很不錯,
第一個搜尋結果(註 3)就是我需要的範本。
但仔細研究以後發現三個問題:
- 我想讓刮刮樂好刮一點,讓滑鼠經過之後就開始刮,而不需要點下滑鼠左鍵。
- 我的刮刮樂圖形是圓的,且有透明背景,套用在上面時透明部分會刮出不必要的顏色來。
- 刮完後我想用 CSS 在底圖上加上簡單動畫特效,底圖若載入 canvas 內就無法做到。
於是我做了一些修改:
首先我把程式裡處理 mouseup
/mousedown
的部分刪去,只留下 mousemove
的部分,
用來判斷是否按下滑鼠的 flag 則一律設定成 true,如此一來就可以直接刮不需要按滑鼠左鍵。
接下來我把原本初始化時,傳進的前後景兩張圖的部分,改成只剩下刮刮樂的前景,
讓 canvas
只用在處理刮除的前景部分,而背景則放在 canvas
外的另一個元素中。
最後因為原腳本中兩張圖時用的合成模式是 source-atop
,
這部分改成前面提到的 destination-out
。並把原本處理兩張圖合成的部分刪去。
如此一來就可以做出刮刮樂(橡皮擦)的效果。
有了這段刮刮樂腳本後,只要像 fiddle 中那樣把 HTML 元素和其 CSS 寫好,
再加入這段程式腳本,你就有一個基本的網頁刮刮樂了!
var scratcher = new Scratcher('fx-scratcher'); scratcher.addEventListener('imagesloaded', scratcherLoadingHandler); var scratcherImage = '/static/img/event/every-moment/scratch/scratch-gray.png'; scratcher.setImages(scratcherImage);
上面的腳本中 'fx-scratcher'
是 canvas
元素的 id
。你也可以換成自訂的 id
。
而 scratcherImage
是刮刮樂圖片(灰色圓形圖)的檔案路徑。
在前面的 fiddle 中因為 canvas
載入的外部圖片會因為安全性限制,
而無法進行我們下面要接著介紹的「取得刮除進度」。
所以我改成用 data url 的方式嵌入圖片,若在本機用自己的圖測試則用檔案路徑即可。
以上只點出修改的重點,完整的變動差異比較可以在這個 Gist Revisions 裡看到。
取得刮除進度
Scratcher.prototype.fullAmount = function (stride) { var i, l; var can = this.canvas.main; var ctx = can.getContext('2d'); var count, total; var pixels, pdata; if (!stride || stride < 1) { stride = 1; } stride *= 4; // 4 elements per pixel pixels = ctx.getImageData(0, 0, can.width, can.height); pdata = pixels.data; l = pdata.length; // 4 entries per pixel total = (l / stride) | 0; for (i = count = 0; i < l; i += stride) { if (pdata[i] != 0) { count++; } } return count / total; };
這個函式是原本的範本程式裡就寫好的,用途是取得目前刮剩下的部分比例。
我只把原本前面的 canvas.draw
改成 canvas.main
,
因為前面修改過後我們只需要一個 canvas
即可。
這段程式腳本的重點是透過 ctx.getImageData
將圖片轉為陣列資料,
再用迴圈去檢查陣列內還留有顏色的比例。stride
參數是用來設定檢查的頻率,數字愈大則檢查的頻率愈低,
避免每次都要掃過所有資料影響執行效能。
若 pdata[i] = 0
代表已被刮除。
腳本中的函式最後回傳的是一個介於 1 ~ 0 之間的小數,
代表的意義是「未刮除部分所佔比例」。
function scratcherProgressHandler(ev) { // Test every pixel. Very accurate, but might be slow on large // canvases on underpowered devices: //var pct = (scratcher.fullAmount() * 100)|0; // Only test every 32nd pixel. 32x faster, but might lead to // inaccuracy: var pct = (this.fullAmount(32) * 100) | 0; if (pct < 3) { if (!$('.scratcher').hasClass('complete')) { $('.scratcher').addClass('complete'); if (!$('#moment').hasClass('appear')) { $('#moment').addClass('appear') } } } } scratcher.addEventListener('scratch', scratcherProgressHandler);
瞭解 fullAmount
的運作方式和回傳值意義之後,
接著只要在 scratch
事件發生時去檢查 fullAmount
的回傳值,
即可依據目前的刮除進度做出對應的效果。
上面這段腳本是判斷剩餘未刮除的部分小於 3% 時就當作刮除完成,
並在刮刮樂元素上加上 'complete'
的 class
,
接下來的「開獎」效果只要透過 CSS 來處理即可。
One more thing – 做個假掰的開獎效果
我要做的開獎效果分成兩個部分,先是讓剩下沒刮完的部分淡出消失,
再來就是讓 Firefox 的圖示放大淡出,以做出開啟 App 的效果。
.scratcher #fx-scratcher { transition: opacity 0.5s ease-out 0s; } .scratcher.complete #fx-scratcher { opacity: 0; } .scratcher #fx-icon { transition: all 0.5s ease-out 0.5s; left: 0; top: 0; width: 125px; height: 125px; } .scratcher.complete #fx-icon { width: 300px; height: 300px; opacity: 0; left: -83px; top: -100px; }
要用 CSS 達成這些效果就要靠 CSS 的轉場功能 - 也就是 transition
啦!
這裡腳本中使用的 transition 屬性是四個轉場屬性的簡易表示式,
transition: opacity 0.5s ease-out 0s;
這行裡指定的屬性依序為轉場套用屬性 (transition-property)、轉場持續時間 (transition-duration)、
轉場速率變化曲線函式 (transition-timing-function)、轉場延遲時間 (transition-delay)。
要注意的是 transition-property 若在不需要太多屬性同時進行轉場的狀況下,
建議明確列出所有需要轉場的屬性(而不要使用 all),以防執行效能被拖慢。
詳細的說明可參考 MDN 的 CSS 轉場文件(註 4)。
寫好轉場效果的設定後,接下來就是在 .scratcher.complete
CSS 選擇器底下,
指定各別元素的樣式變化。只需要簡單幾行 CSS 就能達成。
完成之後,只要當刮除進度超過 97 %,並執行到這行腳本時:
$('.scratcher').addClass('complete');
CSS 就會偵測到 class 的改變,並開始執行漸變轉場效果。
是不是很有趣呢?你也來試著動手做做看吧!
參考連結:
- 註 1:線上活動:Firefox OS 讓你儘情享受每一刻
- 註 2:MDN – Canvas Tutorial – Compositing
- 註 3:HTML5 Canvas and globalCompositeOperation
- 註 4:MDN – CSS 轉場