canvas+vue實現60幀FPS的搶金幣動畫(類天貓紅包雨)

先看看咱們要作的效果



1、canvas動畫核心概念

徹底沒有canvas基礎的同窗建議先刷一下 [Canvas的基本用法 - Web API 接口參考 | MDN]

重點是理解canvas動畫的基本步驟,在[基本的動畫 - MDN]中,動畫分爲4步走
javascript


初學者能夠再簡單一些,咱們先無論狀態保存,直接兩步走: 前端

  • 清空canvas 
  • 繪製新的一幀動畫 

 用定時器或者window.requestAnimationFrame定時重複以上兩步便可
vue


2、搶金幣核心原理

想象一下整個業務場景,咱們先梳理出3個要解決的核心問題: java

  • 一、生成紅包,這裏有兩種解決方案 
    •  統一輩子成全部的紅包對象,從上到下分佈在y軸,觸發運動後後總體向下運動 
    •  在屏幕上方持續生成新紅包對象,紅包一旦生成,馬上開始運動(本次選擇此方案) 
  • 二、運動,canvas動畫原理 
  • 三、用戶點擊紅包,計算是否點中紅包(事件只能綁定在canvas這一層,須要根據點擊位置進行計算)


3、核心功能

  • 一、預緩存圖片/離屏canvas 
  • 二、canvas繪製多圖,改變每一幀造成動畫 
  • 三、判斷點擊位置,冒泡+1效果


下面都是基於vue的代碼,不能直接跑的,主要用於理解核心功能git

最好是本身理解核心原理後親自動手作個最簡單的demo,有助於加深理解
github

一、預緩存圖片/離屏canvas

頁面上感受有不少不少金幣在按各類角度掉落 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 

這裏咱們直接把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


還記得剛開始說的動畫核心兩步走嗎 

  • 清空canvas 
  • 繪製新的一幀動畫

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


到這裏,運動過程就已經結束了,先總結一下上面的內容 

  • 一、初始化canvas 
  • 二、緩存金幣圖片,生成金幣對象,每一個金幣對象包含自身信息 
  • 三、不斷生成金幣對象,並增長到要遍歷運動的數組this.coinArr 
  • 四、經過window.requestAnimationFrame,每一幀都用canvas從新遍歷繪製this.coinArr,每一幀都改變this.coinArr裏面的每個對象的y值大小,造成運動感


三、判斷點擊位置,冒泡+1效果

經過上面的效果圖,咱們能夠看到,點擊金幣時,對應的這個金幣會消失(若是有重疊,只會消失最上面的那個金幣),並且還會有個+1的效果,並緩慢上移消失


先思考一下邏輯 

  • 一、綁定點擊事件 
  • 二、計算位置,遍歷當前整個金幣數組,看看點擊在哪一個金幣上,找出最上面那個,而後刪除這個金幣對象 
  • 三、在點擊位置上,繪製一個+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);
},複製代碼


4、性能測試

到這裏,整個運動的核心原理就講完了,咱們測試一下動畫的性能 

在chrome的性能測試裏面能夠看到,整個運動過程的fps穩穩保持在60幀每秒,能夠說是性能很不錯了 



後話

感謝您耐心看到這裏,但願有所收穫!

我在學習過程當中喜歡作記錄,分享的是本身在前端之路上的一些積累和思考,但願能跟你們一塊兒交流與進步,更多文章請看【amandakelake的Github博客】

相關文章
相關標籤/搜索