上一章講解了如何使用 canvas 實現大轉盤抽獎點擊回顧;但有些地方並無講清楚,好比上一章實現的大轉盤,獎品選項只能填文字,而不能放圖片上去。 這一次,咱們用 canvas 來實現九宮格抽獎(我已沉迷抽獎沒法自拔~),順便將渲染圖片功能也給你們過一遍。 本章涉及到的知識點,主要有:javascript
context.drawImage()
方法渲染圖片context.isPointInPath()
方法,在畫布中製做按鈕setTimeout()
方法,來作逐幀動畫由於本章代碼比較繁雜,我不會所有貼出來;建議進入個人 Github 倉庫,找到 test 文件下的 sudoku文件夾下載,本章講解的代碼都在裏面啦。java
|--- js
|--- | --- variable.js # 包含了全部全局變量
|--- | --- global.js # 包含了本項目所用到的公用方法
|--- | --- index.js # 九宮格主體邏輯代碼
|--- index.html
複製代碼
首先,咱們須要繪製出一個九宮格,你們都知道九宮格長什麼樣子哈,簡單的排9個方塊,不就搞定了麼?git
不不不,做爲一名合格的搬磚工,咱們須要嚴於律己,寫代碼要抽象,要能重用,要... 假如哪天產品大大說,我要12宮格兒的,15的,20的,你咋辦,一個個從新算額~ 因此,咱們得作成圖1這樣的:github
敲敲數字,鳥槍變大炮。無論你9宮仍是12宮仍是自宮,哥都不怕。算法
如下是個人實現方法,若是你們有更簡單的方法,請告訴我,請告訴我,請告訴我,學美術出生的我數學真的很爛~gulp
咱們將九宮格看作一個完整的矩形,矩形有四個頂點; 假設每一行每一列,咱們只顯示3個小方塊(也就是傳統的九宮格),那麼四個頂點上的小方塊序號分別是,0, 2, 4, 6
; 假設每一行每一列,咱們顯示4個小方塊,那麼四個頂點上的小方塊序號分別是,0, 3, 6, 9
; 以此類推,每行每列顯示5個小方塊,就是 0, 4, 8, 12
;canvas
每行每列小方塊數量 | 左上角 | 右上角 | 右下角 | 左下角 |
---|---|---|---|---|
3個 | 0 | 2 | 4 | 6 |
4個 | 0 | 3 | 6 | 9 |
5個 | 0 | 4 | 8 | 12 |
如圖2:跨域
聰明的小夥伴們應該已經發現規律了,在圖1中,咱們使用的神祕變量 AWARDS_ROW_LEN
,它的做用就是指定九宮格每行每列顯示多少個小方塊;數組
接着,咱們繪製的原理是:分紅四步,從每個頂點開始繪製小方塊,直到碰到下一個頂點爲止;
咱們會發現,當 AWARDS_ROW_LEN = 3
時,咱們從 0 ~ 1
,從 2 ~ 3
... ,每一次繪製兩個小方塊;
當 AWARDS_ROW_LEN = 4
時,咱們從0 ~ 2
,從 3 ~ 5
,每一次繪製三個小方塊,繪製的步數恰好是 AWARDS_ROW_LEN - 1
;如圖3:
AWARDS_TOP_DRAW_LEN
,來表示不一樣狀況下,每一個頂點繪製的步數;
咱們經過 AWARDS_TOP_DRAW_LEN
這個變量,又能夠推算出,任何狀況下,矩形四個頂點所在的小方塊的下標:
你能夠列舉多種狀況,來驗證該公式的正確性
LETF_TOP_POINT = 0,
RIGHT_TOP_POINT = AWARDS_TOP_DRAW_LEN,
RIGHT_BOTTOM_POINT = AWARDS_TOP_DRAW_LEN * 2,
LEFT_BOTTOM_POINT = AWARDS_TOP_DRAW_LEN * 2 + AWARDS_TOP_DRAW_LEN,
複製代碼
獲得了每一個頂點的下標,那就意味着咱們知道了一個頂點距離另外一個頂點之間,有多少個小方塊,那麼接下來就很是好辦了,
AWARDS_TOP_DRAW_LEN
乘以4,來獲取總的獎品個數,做爲循環條件(AWARDS_LEN
);SUDOKU_SIZE
);SUDOKU_ITEM_MARGIN
);SUDOKU_ITEM_SIZE
)。變量有點多·若是你感受有點懵逼,請仔細查閱源碼
variable.js
中的變量,搞懂每一個變量的表明的意義。
咱們已經拿到全部繪製的條件,接下來只須要寫個循環,輕鬆搞定!
function drawSudoku() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < AWARDS_LEN; i ++) {
// 頂點的座標
let max_position = AWARDS_TOP_DRAW_LEN * SUDOKU_ITEM_SIZE + AWARDS_TOP_DRAW_LEN * SUDOKU_ITEM_MARGIN;
// ----- 左上頂點
if (i >= LETF_TOP_POINT && i < RIGHT_TOP_POINT) {
let row = i,
x = row * SUDOKU_ITEM_SIZE + row * SUDOKU_ITEM_MARGIN,
y = 0;
// 記錄每個方塊的座標
positions.push({x, y});
// 繪製方塊
drawSudokuItem(
x, y, SUDOKU_ITEM_SIZE, SUDOKU_ITEM_RADIUS,
awards[i], SUDOKU_ITEM_TXT_SIZE, SUDOKU_ITEM_UNACTIVE_TXT_COLOR,
SUDOKU_ITEM_UNACTIVE_COLOR,
SUDOKU_ITEM_SHADOW_COLOR
);
};
// -----
// ----- 右上頂點
if (i >= RIGHT_TOP_POINT && i < RIGHT_BOTTOM_POINT) {
// ...
};
// -----
// ----- 右下頂點
if (i >= RIGHT_BOTTOM_POINT && i < LEFT_BOTTOM_POINT) {
// ...
}
// -----
// ----- 左下頂點
if (i >= LEFT_BOTTOM_POINT) {
// ...
}
// -----
};
}
複製代碼
在繪製九宮格的 drawSudoku()
函數方法中,你會發現,咱們每一步繪製,都將當前小方塊的座標推到了一個 positions
的全局變量中;
這個變量會記錄全部小方塊的座標,以及他們的下標;
以後咱們在繪製輪跳的小方塊時,就可以經過 setTimeout()
定時器,規定每隔一段時間,經過下標值 jump_index
取出 positions
變量中的某一組座標信息,並經過該信息中的座標繪製一個新的小方塊,覆蓋到原來的小方塊上,結束繪製後,jump_index
的值遞增;
這便實現了九宮格的輪跳效果。
而繪製這些小方塊,咱們封裝了一個公共的方法:drawSudokuItem()
;
/** * 繪製單個小方塊 * @param {Num} x 座標 * @param {Num} y 座標 * @param {Num} size 小方塊的尺寸 * @param {Num} radius 小方塊的圓角大小 * @param {Str} text 文字內容 * @param {Str} txtSize 文字大小樣式 * @param {Str} txtColor 文字顏色 * @param {Str} bgColor 背景顏色 * @param {Str} shadowColor 底部厚度顏色 */
function drawSudokuItem(x, y, size, radius, text, txtSize, txtColor, bgColor, shadowColor) {
// ----- 繪製方塊
context.save();
context.fillStyle = bgColor;
context.shadowOffsetX = 0;
context.shadowOffsetY = 4;
context.shadowBlur = 0;
context.shadowColor = shadowColor;
context.beginPath();
roundedRect(
x, y,
size, size,
radius
);
context.fill();
context.restore();
// -----
// ----- 繪製圖片與文字
if (text) {
if (text.substr(0, 3) === 'img') {
let textFormat = text.replace('img-', ''),
image = new Image();
image.src = textFormat;
function drawImage() {
context.drawImage(
image,
x + (size * .2 / 2), y + (size * .2 / 2),
size * .8, size * .8
);
};
// ----- 若是圖片沒有加載,則加載,如已加載,則直接繪製
if (!image.complete) {
image.onload = function (e) {
drawImage();
}
} else {
drawImage();
}
// -----
}
else {
context.save();
context.fillStyle = txtColor;
context.font = txtSize;
context.translate(
x + SUDOKU_ITEM_SIZE / 2 - context.measureText(text).width / 2,
y + SUDOKU_ITEM_SIZE / 2 + 6
);
context.fillText(text, 0, 0);
context.restore();
}
}
// -----
}
複製代碼
該方法是一個公共的繪製小方塊的方法,它能在初始化時繪製全部「底層」小方塊,在動畫輪跳是,繪製那個移動中的小方塊。
drawSudokuItem() 實現了哪些功能?
global.js
中的一個 roundedRect()
方法,繪製了一個圓角矩形;(本章暫不討論圓角矩形的繪製方法,若是你感興趣,能夠查看源碼,或者 GG 一下)awards
數組來存儲獎品信息,若是值是普通的字符串,則在小方塊的正中繪製該字符串文字,若是值帶有前綴 img-
咱們就將該字符串中的 url 地址,做爲圖片的地址,渲染到小方塊上。繪製方塊沒啥好講的,若是你不想用 roudedRect()
方法,你能夠直接把它替換成 context.rect()
,除了不是圓角,效果徹底同樣。
context.drawImage()
這個方法:先清楚一個概念:
source image
;destination canvas
。語法:
context.drawImage(
HTMLImageElement $image,
int $sourceX, int $sourceY [ , int $sourceW, int $sourceH,
int $destinationX, int $destinationY, int $destinationW, int $destinationH ]
)
複製代碼
參數有點多哈,但本章用到的也就前五個,其中前三個是必選,後兩個是可選參數:
$image # 能夠是 HTMLImageElement 類型的圖像對象,
# 也能夠是 HTMLCanvasElement 類型的 canvas 對象,
# 或 HTMLVideoElement 類型的視頻對象
# 也就是說,它能夠將指定 圖片,canvas,視頻 繪製到指定的 canvas 畫布上。
# 能夠看到,該方法能夠繪製另外一個 canvas,
# 咱們能夠經過這個特性實現 離屏canvas;在之後的章節中我會詳細的講解。
$sourceX / Y # 源圖像的座標,用這兩個參數控制圖片的座標位置。
$sourceW / H # 源圖像的寬高,用這兩個參數控制圖片的寬度與高度。
複製代碼
⚠️ 這個方法有兩個坑:
image.onload = function(e) {...}
回調中調用 context.drawImage()
。若是你不知道怎麼創建本地服務器的話,我...,憤怒的我當場百度了一篇最簡單搭建服務器的教程,童叟無欺!gulp 搭建本地服務器教程
咱們來看如下代碼:
if (text.substr(0, 3) === 'img') {
let textFormat = text.replace('img-', ''),
image = new Image();
image.src = textFormat;
function drawImage() {
context.drawImage(
image,
x + (size * .2 / 2), y + (size * .2 / 2),
size * .8, size * .8
);
};
// ----- 若是圖片沒有加載,則加載,如已加載,則直接繪製
if (!image.complete) {
image.onload = function (e) {
drawImage();
}
} else {
drawImage();
}
// -----
}
複製代碼
img
,若是有,便開始繪製圖片;image
對象,將該對象的 src
屬性賦值;drawImage()
函數方法,在該方法裏面,使用 context.drawImage()
方法渲染剛剛定義的 image
對象,並指定相應的圖片大小,和尺寸;image.complete
來判斷圖片是否已加載完成,若是未加載,則先初始化,在 image.onload = function(e) {...}
的回調中調用 drawImage()
方法;若是已經加載完畢,則直接調用 drawImage()
方法。以上,圖片就這樣渲染完成了,渲染普通文本就不用說了哈,就是普通的 context.fillText()
方法。
咱們已經將外圍的小方塊繪製完成了,接下來來製做中間的按鈕。
按鈕的繪製很簡單,你們看看源碼, 就能輕鬆理解;
可是這個按鈕在 canvas 中,只不過就是一堆像素組成的色塊,它不能像 html
中定義的按鈕那樣,具備點擊,鼠標移動等交互功能;
若是咱們想在 canvas 中實現一個按鈕,那咱們只能規定當咱們點擊 canvas 畫布中的某一個區域時,給予用戶反饋;
🎉 這裏引入一個新的方法,
context.isPointInPath()
; 人如其名,該方法會判斷:當前座標點,是否在當前路徑中,若是在,返回 true,不然返回 false。
語法:
context.isPointInPath(int $currentX, int $currentY)
複製代碼
兩個參數就表明須要進行判斷的座標點。
經過這個方法,咱們能夠判斷:當前用戶點擊的位置的座標,是否位於按鈕的路徑中,若是返回 true,則執行抽獎動畫。
⚠️ 值得注意的是,判斷的路徑,必須是當前路徑,也就是說,咱們在執行判斷以前須要從新繪製一遍按鈕的路徑;源碼中的
createButtonPath()
就是爲了作這件事情存在的。
咱們來作一個簡單的小測試,測試效果如圖4:
var canvas = document.getElementById('canvas'),
context = canvas.getContext('2d');
function windowToCanvas(e) {
var bbox = canvas.getBoundingClientRect(),
x = e.clientX,
y = e.clientY;
return {
x: x - bbox.left,
y: y - bbox.top
}
}
context.beginPath();
context.rect(100, 100, 100, 100);
context.stroke();
canvas.addEventListener('click', function (e) {
var loc = windowToCanvas(e);
if (context.isPointInPath(loc.x, loc.y)) {
alert('🎉')
}
});
複製代碼
怎麼樣?炒雞簡單對吧?在咱們這個項目中也是同樣的:
button_position
這個變量中;當前路徑
,咱們將點擊事件 click
中獲取的座標信息傳給 context.isPointInPath()
方法,就能夠判斷,當前的位置,是否在按鈕的路徑中。['mousedown', 'touchstart'].forEach((event) => {
canvas.addEventListener(event, (e) => {
let loc = windowToCanvas(e);
// 建立一段新的按鈕路徑,
createButtonPath();
// 判斷當前鼠標點擊 canvas 的位置,是否在當前路徑中,
// 若是爲 true,則開始抽獎
if (context.isPointInPath(loc.x, loc.y) && !is_animate) {
// ...
}
})
});
複製代碼
咱們將經過點擊按鈕,來調用 animate()
方法,該方法實現了九宮格抽獎的動畫效果。
在點擊按鈕時,咱們會初始化三個全局變量,jumping_time, jumping_total_time, jumping_change
;
它們分別表示:動畫當前時間計時;動畫花費的時間總長;動畫速率改變的峯值(使用 easeOut
函數方法,單位時間內會將速率由0提高到峯值);
最後咱們將調用 animate()
函數方法,如下是該方法的代碼:
function animate() {
is_animate = true;
if (jump_index < AWARDS_LEN - 1) jump_index ++;
else if (jump_index >= AWARDS_LEN -1 ) jump_index = 0;
jumping_time += 100; // 每一幀執行 setTimeout 方法所消耗的時間
// 當前時間大於時間總量後,退出動畫,清算獎品
if (jumping_time >= jumping_total_time) {
is_animate = false;
if (jump_index != 0) alert(`🎉恭喜您中得:${awards[jump_index - 1]}`)
else if (jump_index === 0) alert(`🎉恭喜您中得:${awards[AWARDS_LEN - 1]}`);
return;
};
// ----- 繪製輪跳方塊
drawSudoku();
drawSudokuItem(
positions[jump_index].x, positions[jump_index].y,
SUDOKU_ITEM_SIZE, SUDOKU_ITEM_RADIUS,
awards[jump_index], SUDOKU_ITEM_TXT_SIZE, SUDOKU_ITEM_ACTIVE_TXT_COLOR,
SUDOKU_ITEM_ACTIVE_COLOR,
SUDOKU_ITEM_SHADOW_COLOR
);
// -----
setTimeout(animate, easeOut(jumping_time, 0, jumping_change, jumping_total_time))
}
複製代碼
animate() 函數方法:
is_animate
,該變量用來阻止用戶在動畫進行時反覆點擊按鈕,使動畫不斷被調用;該變量初始值爲 false
,僅當該變量爲 false
時,點擊按鈕纔會進入 animate()
函數;當進入 animate()
函數後,該變量被設置爲 true
,結束動畫時,又被重置爲 false
;jump_index
全局變量的初始值是一個小於等於獎品總數的隨機正整數;隨着每一幀動畫的執行遞增,但當他等於獎品總數時,又會被重置爲 0,以此循環;咱們使用該變量,來繪製輪跳的小方塊;jumping_time
全局變量初始值爲0,隨着每一幀動畫的執行遞增,以此來記錄動畫當前時間點,當這個值大於等於時間總量 jumping_total_time
時,就能夠結束動畫,並將當前的 jump_index
取出,做爲抽中的獎品了;drawSudoku()
方法中第一句代碼就是:context.clearRect(0, 0 , canvas.width, canvas.height)
;它用於清理整個畫板,並將九宮格重繪出來;drawSudokuItem()
咱們使用這個函數方法,來繪製輪跳的小方塊;前面說過,咱們將 jump_index
作爲下標,那麼咱們就能夠在 positions
變量中找到座標信息,從 awards
變量中,找到獎品信息;setTimeout()
方法,來實現小方塊的動畫;該方法調用 animate()
方法自己,它的第二個參數,咱們使用了上一章介紹過的緩動函數來定義,這會使動畫看上去由快到慢;緩動函數的源碼能夠在 global.js
中找到。O 啦~全部代碼講解完畢,你的九宮格是否也動起來了?😍
canvas 實現動畫的方式不外乎就是清除畫板,再從新繪製一個 動做
,理解了它,不管你是用 window.requestAnimateFrame()
仍是 setTimeout() 和 setInterval()
來作動畫,都是同樣的原理;
九宮格的實現很簡單,惟一複雜點的,就須要一系列計算,來繪製一個靈活的九宮格;
九宮格不只能夠用來抽獎,也能夠用來作一些小遊戲,還記得小時候玩過的老虎機麼?如圖5:
改改樣式,找點圖片,把值取出來作下分數規則判斷,分分鐘搞定呢!