1. 程式人生 > >用canvas實現紅心飄飄的動畫效果

用canvas實現紅心飄飄的動畫效果

兩週前,專案裡需要實現一個紅心飄飄的點贊效果。抓耳撓腮了老半天,看了幾篇大佬的文章,終於算是摸了個七七八八。不禁長嘆一聲,還是菜啊。先來看一下效果:(傳送門進去點一波)
kiss.gif

一、Bezier曲線運動軌跡

其實用大白話描述一下需求就是讓一個紅心圖片沿著貝塞爾曲線的軌跡走,然後邊走邊消失。核心在於得到貝塞爾曲線上的一系列點。本文不會講解貝塞爾曲線的原理,因為大佬們已經講過了,而且講的比我好。參考文章如下:

其中第二篇文章講到了生成二階和三階貝塞爾曲線可以使用canvas自帶的方法:quadraticCurveTo

bezierCurveTo,而高階的則先得到曲線上一系列的點,然後順次連線這些點來擬合高階的貝塞爾曲線。沒錯,我們要的就是這一系列的點,有了這些點,就可以控制紅心的軌跡了。下面是我基於作者的BezierMarker.js寫的一個demo,可以直觀地看出高階貝塞爾曲線上的點:

上面100個曲線上的點座標是由下面這段程式碼計算得出的:

BezierMaker.prototype.bezier = function(t) { //貝塞爾公式呼叫
    var x = 0,
        y = 0,
        bezierCtrlNodesArr = this.bezierCtrlNodesArr,
        n = bezierCtrlNodesArr.length - 1,
        self = this
    bezierCtrlNodesArr.forEach(function(item, index) {
        if(!index) {
            x += item.x * Math.pow(( 1 - t ), n - index) * Math.pow(t, index) 
            y += item.y * Math.pow(( 1 - t ), n - index) * Math.pow(t, index) 
        } else {
            x += self.factorial(n) / self.factorial(index) / self.factorial(n - index) * item.x * Math.pow(( 1 - t ), n - index) * Math.pow(t, index) 
            y += self.factorial(n) / self.factorial(index) / self.factorial(n - index) * item.y * Math.pow(( 1 - t ), n - index) * Math.pow(t, index) 
        }
    })
    return {
        x: x,
        y: y
    }
}

這個方法就是對貝塞爾公式的實現。以3階貝塞爾公式為例(見下圖),它的方程需要四個控制點(P1,P2,P3,P4)和一個t值,就能計算出曲線上的某一點的座標。


根據給定的t值,結合控制點的座標,算出相應t值下的貝塞爾曲線上的點的座標。拿下圖(來自第一篇文章)來說,給定t值為0.25,就可以得到B點的座標

img

當將t由0遞增到1時,就可以得到100個曲線上的點,進而擬合出相應的曲線。當我們拿到這一系列點時,其實問題已經解決了一大半了。

二、使紅心飄起來

拿到擬合點陣列後,繪製軌跡就是從陣列中依次拿出座標,並將紅心圖片繪製到相應的座標上。並根據當前擬合點在曲線陣列中的位置,改變圖片的不透明度,就可以讓紅心飄起來了,上一部分程式碼,講解見註釋:

// 生成隨機數
function rnd () {
  let flag = Math.random() > 0.5 ? 1 : -1
  return 80 * Math.random() * flag
}

class FlyHeart {
  constructor (ctx, img) {
    this.ctx = ctx;
    this.img = heart;
    // 拿到紅心的運動軌跡,一系列擬合點座標
    this.bezierArr = new BezierMaker(ctx, [
      {x: 187, y: 245},
      {x: 170 + rnd(), y: 200},
      {x: 200 + rnd() , y: 120}, 
      {x: 140 + rnd(), y: 60}], 90).bezierArr //90表示擬合點的數量,rnd使紅心的軌跡有一定的隨機性
  }
  draw () {
    // 依次取出軌跡的每個點
    let position = this.bezierArr.shift();
    
    // 清除上次畫的
    this.clear();
    
    if (position) {
      this.ctx.save()
      // 根據當前陣列長度算出透明度
      this.ctx.globalAlpha = this.bezierArr.length / 30;
      this.ctx.drawImage(this.img, position.x , position.y, 20, 20);
      this.ctx.restore();
      this.prevPosition = position;
    }
  }
  // 清除上次畫的
  clear () {
    if (this.prevPosition) {
      this.ctx.clearRect(this.prevPosition.x, this.prevPosition.y, 20, 20);
    }
  }
}

接下來就是給body新增點選事件,當點選時,就新生成一個紅心:

  document.body.addEventListener('click', function() {
    heartArr.push(new FlyHeart(ctx, heart));
  })
  
  let heartArr = []
  const cvs = document.getElementById('cvs')
  const ctx = cvs.getContext('2d')
  const heart = document.getElementById('heart') //圖片
  
  function draw () {
    if(heartArr.length) {
      for(let heart of heartArr) {
        heart.draw();
        if(heart.bezierArr.length === 0) {
          heart.clear();
          let index = heartArr.indexOf(heart)
          heartArr.splice(index, 1)
        }
      }
    }
    requestAnimationFrame(draw)
  }
  draw()

三、後記

當時看到這個需求的時候,真的是一籌莫展,看到n階貝塞爾曲線時更是一頭霧水,但是看不懂也要看,然後看著看著,看多了也就慢慢明白了。希望沒浪費大家的時間,各位看官看完後有所收穫(完)