徹底沒有canvas基礎的同窗建議先刷一下 [Canvas的基本用法 - Web API 接口參考 | MDN]
重點是理解canvas動畫的基本步驟,在[基本的動畫 - MDN]中,動畫分爲4步走
javascript
初學者能夠再簡單一些,咱們先無論狀態保存,直接兩步走: 前端
用定時器或者window.requestAnimationFrame
定時重複以上兩步便可
vue
想象一下整個業務場景,咱們先梳理出3個要解決的核心問題: java
下面都是基於vue的代碼,不能直接跑的,主要用於理解核心功能git
最好是本身理解核心原理後親自動手作個最簡單的demo,有助於加深理解
github
頁面上感受有不少不少金幣在按各類角度掉落 chrome
其實頁面上一共就4種金幣圖片,只是他們的大小、速度不同,看起來有每個都不同 canvas
咱們能夠先把這4張圖片全都加載好
api
// 緩存幾種金幣圖片爲DOM元素,避免canvas繪製時還須要異步讀取圖片
loadImgs(arr) {
return new Promise(resolve => {
let count = 0;
// 循環圖片數組,每張圖片都生成一個新的圖片對象
const len = arr.length;
for (let i = 0; i < len; i++) {
// 建立圖片對象
const image = new Image();
// 成功的異步回調
image.onload = () => {
count++;
arr.splice(i, 1, {
// 加載完的圖片對象都緩存在這裏了,canvas能夠直接繪製
img: image,
// 這裏能夠直接生成並緩存離屏canvas,用於優化性能,但本次不用,只是舉個例子
offScreenCanvas: this.createOffScreenCanvas(image)
});
// 這裏說明 整個圖片數組arr裏面的圖片全都加載好了
if (count == len) {
this.preloaded = true;
resolve();
}
};
image.src = arr[i].img;
}
});
},
複製代碼
建立離屏canvas的方法以下
數組
createOffScreenCanvas(image) {
const offscreenCanvas = document.createElement("canvas");
const offscreenContext = offscreenCanvas.getContext("2d");
// 這裏能夠是動態寬高
offscreenContext.width = 30;
offscreenContext.height = 30;
offscreenContext.drawImage(
image,
0,
0,
offscreenContext.width,
offscreenContext.height
);
// return這個offscreenCanvas
return offscreenCanvas;
},複製代碼
首先初始化canvas
這裏咱們直接把canvas的上下文ctx
存在data
裏面,方便在各個方法裏面讀取。
在vue裏面寫不像單獨的一個JS模塊,能夠用閉包來封裝一個獨立的上下文,而在vue裏面也不建議聲明全局變量
initCanvas() {
const canvas = document.getElementById("canvas");
if (canvas.getContext) {
this.ctx = canvas.getContext("2d");
// 初始化時同步進行圖片預加載
this.loadImgs(this.imgArr);
}
},複製代碼
繪製多圖,其實就是循環遍歷上面建立好的圖片數組imgArr
,而後對於每一個圖片對象,都調用this.ctx.drawImage()
方法便可
下面咱們把圖片轉變化金幣對象
把圖片數組imgArr
替換成金幣對象數組coinArr
,這個數組是由一個個的金幣對象Coin
組成,金幣對象自身除了有圖片,還有大小、物理位置、下落速度等參數,也就是說,每一個金幣對象緩存本身的全部繪製信息,這裏用的是面向對象的思惟
const Coin = {
x: 'x軸位置',
y: 'y軸位置', // 運動的關鍵是在每一幀都改變y
radius: '金幣大小',
img: '前面緩存好的金幣圖片',
speed: '金幣的下落速度'
};複製代碼
每一幀,循環這個金幣數組,而後繪製出全部的金幣對象
若是要運動起來,每一幀讓每一個金幣的y軸位置往下掉一點,就是這句y: coin.y + coin.speed
那麼繪製下一幀時,其餘信息都不變,每一個金幣都往下移動了一點點,連貫起來,這不一樣的一幀一幀組合起來就成了運動的動畫了
先看繪製的代碼
drawCoins() {
// 遍歷這個金幣對象數組
this.coinArr.forEach((coin, index) => {
const newCoin = {
x: coin.x,
// 運動的關鍵 每次只有y不同
y: coin.y + coin.speed,
radius: coin.radius,
img: coin.img,
speed: coin.speed
};
// 繪製某個金幣對象時,也同時生成一個新的金幣對象,替換掉原來的它,惟一的區別就是它的y變了,下一幀繪製這個金幣時,就運動了一點點距離
this.coinArr.splice(index, 1, newCoin);
this.ctx.drawImage(
coin.img,
coin.x,
coin.y,
coin.radius,
coin.radius * 1.5
);
});
},複製代碼
那麼怎麼連貫運動起來呢,不斷的執行this.drawCoins()方法便可
既然作動畫,咱們確定得知道【window.requestAnimationFrame】這個api
還記得剛開始說的動畫核心兩步走嗎
moveCoins() {
// 清空canvas
this.ctx.clearRect(0, 0, this.innerWidth, this.innerHeight);
// 繪製新的一幀動畫
this.drawCoins();
// 不斷執行繪製,造成動畫
this.moveCoinAnimation = window.requestAnimationFrame(this.moveCoins);
},複製代碼
到這裏,咱們其實已經能讓金幣運動起來了,不過咱們要作的是讓不少不少金幣不斷的往下掉,因此咱們選擇在運動的過程當中,不斷生成新的金幣對象,而後push到this.coinArr
中
pushCoins() {
// 每次隨機生成1~3個金幣
const random = this.randomRound(3, 6);
let arr = [];
for (let i = 0; i < random; i++) {
// 建立新的金幣對象
const newCoin = {
x: this.random(
this.calculatePos(10),
this.innerWidth - this.calculatePos(150)
), // 橫向隨機 金幣不要貼近邊邊
y: 0 - this.calculatePos(Math.random() * 150), // -150內高度 隨機
radius: this.calculatePos(120 + Math.random() * 30), // 100寬 大小浮動15
img: this.coinObjs[this.randomRound(0, 3)].img, // 隨機取一個金幣圖片對象,這幾個圖片對象在頁面初始化時就已經緩存好了
speed: this.calculatePos(Math.random() * 7 + 5) // 下落速度 隨機
};
arr.push(newCoin);
}
// 每次都插入一批新金幣對象arr到運動的金幣數組this.coinArr
this.coinArr = [...this.coinArr, ...arr];
// 間隔多久生成一批金幣
this.addCoinsTimer = setTimeout(() => {
this.pushCoins();
}, 600);
},複製代碼
由於每一個金幣的初始y的位置都是屏幕上方,因此看起來都是不斷生成金幣而後往下掉的
至於計算大小的方法,這個比較隨意了
最後,把上面的彙總起來,開啓動畫的方法是這樣的
start() {
this.pushCoins(); // 不斷增長金幣
this.moveCoins(); // 金幣開始運動
// 開始10秒倒計時
this.runCountdownTimer = setInterval(() => {
//...倒計時10s後,作一些中止動畫的工做
}, 1000);
},複製代碼
到這裏,運動過程就已經結束了,先總結一下上面的內容
this.coinArr
window.requestAnimationFrame
,每一幀都用canvas從新遍歷繪製this.coinArr
,每一幀都改變this.coinArr
裏面的每個對象的y值大小,造成運動感經過上面的效果圖,咱們能夠看到,點擊金幣時,對應的這個金幣會消失(若是有重疊,只會消失最上面的那個金幣),並且還會有個+1的效果,並緩慢上移消失
先思考一下邏輯
首先,canvas自己就是一個DOM對象,繪製在它上面的金幣並非dom對象,沒法綁定點擊事件,因此只能綁定在canvas上面,經過event
拿到點擊位置,有點事件代理的味道吧
listenClick() { const canvas = document.getElementById("canvas"); canvas.addEventListener("click", e => { const pos = { x: e.clientX, y: e.clientY }; }); },複製代碼
既然拿到此刻的點擊位置,而當前的金幣數組this.coinArr
也知道,數組裏面的每一個金幣對象都維護了自身的信息,其中就包括了位置和金幣大小
那麼,只要遍歷一下,若是點擊位置在這個金幣的大小範圍以內,那麼是否是能夠認爲點擊中了這個金幣?
// 判斷點擊位置 是否處於某個coin之中
isIntersect(point, coin) {
const distanceX = point.x - coin.x;
const distanceY = point.y - coin.y;
const withinX = distanceX > 0 && distanceX < coin.radius;
// 金幣圖片是長方形的 咱們只計算下半部的正方形 不計算金幣尾巴
const withinY =
distanceY > 0 &&
distanceY > coin.radius * 0.5 &&
distanceY < coin.radius * 1.5;
return withinX && withinY;
},複製代碼
但,同一時刻,有可能點中了不少個重疊的金幣,那麼咱們遍歷時,把這幾個金幣都拿出來,只要最上面那個就行了
listenClick() {
const canvas = document.getElementById("canvas");
canvas.addEventListener("click", e => {
// 點擊位置
const pos = {
x: e.clientX,
y: e.clientY
};
// 全部點中的金幣都存這
const clickedCoins = [];
this.coinArr.forEach((coin, index) => {
// 判斷點擊位置是否在該金幣範圍內
if (this.isIntersect(pos, coin)) {
clickedCoins.push({
x: e.clientX,
y: e.clientY,
// 索引很重要,用於刪除this.coinArr內的該金幣
index: index
});
}
});
// 若是點擊中了重疊的金幣,只取第一個便可 也只刪除第一個金幣 count也只增長一次
if (clickedCoins.length > 0) {
this.count += 1;
const bubble = {
x: clickedCoins[0].x,
y: clickedCoins[0].y,
opacity: 1
};
// 這跟生成+1冒泡效果相關,下面立刻講
this.bubbleArr.push(bubble);
// 移除被點中的第一個金幣對象
this.coinArr.splice(clickedCoins[0].index, 1);
}
});
},複製代碼
既然拿到了此刻的位置,在當前位置繪製一個冒泡效果應該不是難事,只要處理好冒泡的移動和消失便可,本質上就跟上面繪製金幣是同樣的
this.bubbleArr
數組,動畫中循環遍歷繪製它裏面的對象bubble
bubble
有位置信息,加多一個透明度opacity
,運動的過程當中,不斷減少透明度,直到變爲0,就把這個bubble
從數組上刪除便可drawBubble() {
this.bubbleArr.forEach((ele, index) => {
if (ele.opacity > 0) {
// 透明度漸變
this.ctxBubble.globalAlpha = ele.opacity;
this.ctxBubble.drawImage(
this.bubbleImage,
ele.x,
ele.y,
this.calculatePos(60),
this.calculatePos(60)
);
// 更新:每次畫完就減小0.02透明度,同時位置移動
const newEle = {
x: ele.x + this.calculatePos(1),
y: ele.y - this.calculatePos(2),
opacity: ele.opacity - 0.02
};
this.bubbleArr.splice(index, 1, newEle);
}
});
},
keepDrawBubble() {
this.ctxBubble.clearRect(0, 0, this.innerWidth, this.innerHeight);
// 把opacity爲0的所有清除
this.bubbleArr.forEach((ele, index) => {
if (ele.opacity < 0) {
this.bubbleArr.splice(index, 1);
}
});
this.drawBubble();
this.bubbleAnimation = window.requestAnimationFrame(this.keepDrawBubble);
},複製代碼
到這裏,整個運動的核心原理就講完了,咱們測試一下動畫的性能
在chrome的性能測試裏面能夠看到,整個運動過程的fps穩穩保持在60幀每秒,能夠說是性能很不錯了
感謝您耐心看到這裏,但願有所收穫!
我在學習過程當中喜歡作記錄,分享的是本身在前端之路上的一些積累和思考,但願能跟你們一塊兒交流與進步,更多文章請看【amandakelake的Github博客】