5分鐘用動效工廠實現粒子動畫

得物技術發表於2022-03-10

本篇文章將使用動效工廠從0-1去搭建並生成各種各樣的粒子動畫, 利用視覺化編輯的能力,而你通過簡易的操作就可以實現, 下面我就帶你一步一步如何去實現一個粒子Action ,準確的來說去根據一個業務實現一個自定義的action。

讀完本篇文章你可以瞭解到下面這幾點:

  1. 如何實現一個自定義action
  2. 如何使用動效工廠

動效工廠

在實現自定義action 之前, 我先簡單的介紹下我們這個專案的背景。什麼是動效工廠?

我們團隊用的動效場景比較多,所以沉澱了一些動效,同時為了下次在遇到可以更好的複用。於是我們能支援多少動效,或者有哪些動效可以被複用,就可以在動效工廠的動效庫裡去找。

這時候就會有同學說這些東西都不滿足需求、不通用,於是就有了接下來要講的的自定義action,也就是實現一個動效庫沒有的動畫,然後再提交到動效庫裡,後面有類似的動效,別人也就可以複用了。

粒子動畫

這裡的話我帶大家實現一個粒子動畫, 首先還是解釋什麼是粒子動畫????

粒子動畫是由在一定範圍內隨機生成的大量粒子產生運動而組成的動畫,被廣泛運用於模擬天氣系統、煙霧光效等方面。在電商平臺的微型遊戲化場景中,粒子動畫主要 用於呈現在能量收集、金幣收集時的特效。

這裡我給大家準備了兩段我們業務場景中使用的例子:

飛書20220309-110541.gif

飛書20220309-111311.gif

上面的粒子還是不夠多,並且給粒子 貼上對應的貼圖,再以某種動畫的去展示,從而達到視覺上感覺非常爽的狀態,就是有一種?好多的感覺!

建立粒子類

其實無論什麼樣的粒子效果,你光看效果你都不知道如何分析這一個動畫。其實分析動畫,我們可以先看一個粒子是怎麼變化的。我們看下面這張圖:

粒子A在畫布上,從A位置經過一定的動畫 可能是線性變化, 也有可能是拋物線,或者是我們的三階貝塞爾曲線變化。 但是萬變不離其宗, 本質上都x 和 y 的變化, 但是x 從 x0 執行到 x1 , y方向 從y0 運動 y1, 那麼其實對應當前這個一個粒子來說 也就是需要有vx 和vy 這兩個屬性。這樣一個粒子最基本的屬性 其實對於基本的動畫已經滿足了。 我們寫下面這段程式碼:

class Particle { 
  constructor(x = 0, y = 0) { 
    this.x = x 
    this.y = x 
    this.radius = 1 
    this.vx = 0 
    this.vy = 0 
    this.color = 'hsla(197,100%,50%,80%)' 
  } 
  
  draw(ctx) { 
    ctx.save() 
    ctx.fillStyle = this.color 
    ctx.beginPath(); 
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); 
    ctx.fill(); 
    ctx.closePath(); 
    ctx.restore() 
  } 
  
  move() { 
    this.x += this.vx 
    this.y += this.vy 
  } 
} 

這樣其實我們就能構造出當前粒子 所需要的特性。但是這時候我們還是需要一個粒子manager去管理所有的粒子,主要是用來

  1. 控制粒子的數量
  2. 生成多個型別的粒子
  3. 執行渲染操作
  4. 控制粒子的 x 方向 速度 和 y方向的速度 在哪一個區間

其實這些就是所謂的統一管理,程式碼就不展示了。其實上面所謂的這些操作 就是構成了粒子action 的一些配置

ACTION編寫

我們開啟到這個目錄

這裡面有我們動效工廠 所有的action,比如我這裡其實就是新建一個 particle-action.ts

這裡先看下 一個action最基本的結構是如何實現的。

就是actionConfig 這裡的話 其實 就是對應到動效工廠 視覺化檢視的哪些屬性,我們會有一些預設屬性, 也就是每個action 固定的一些屬性,但是同時也會有保持特殊的情況。然後你在匯出一個Action 函式

我們看下這裡是如何寫的,這裡其實都是固定寫法。但是我還是想講的明白點:

首先action肯定是作用在檢視上的, 在動效工廠裡其實就是 對應的View

然後就是一些緩動 動畫, 的一些引數,我們看下程式碼

export const particleAction: Action<particleActionConfig> = ( 
  view: View, 
  { timing, color, radius, duration, ...config }, 
) => { 
  let ctx: CanvasRenderingContext2D | null 
  return (time: number) => { 
    const progress = useProgress(time, timing, duration, config) 
    useOnce(() => { 
      ctx = view.makeCanvas().getContext('2d') 
    }) 
    if (ctx) { 
      view.addLayer(ctx.canvas) 
    } 
  } 
} 

這裡用到了 2個 獨立的鉤子

useOnce 和 useProgress

這裡要不得不提了,為啥要用鉤子, 最主要的目的是方便action在呼叫的時候和frame聯動(這裡借鑑了React hooks 的思想)

設定或者獲取當前frame的duration, 我簡單給大家介紹幾個

useCache

快取變數,保持action的執行過程

useState

儲存和修改action的狀態

useOnce

在Frame的整個生命週期中只會被執行一次。

useFinish

在Frame被移除的時候執行

useContext

獲取frame的當前的wind。

useProgress

獲取當前的進度是多少

useOnce

其實有點類似於單例,我們只建立一次,因為上面的函式是每一幀都進行了呼叫, 所以為了避免重複建立所存在的問題

我這裡簡單的說一下,這行程式碼

ctx = view.makeCanvas().getContext('2d')

其實view 只是我們抽象出來的東西,無論你做什麼操作,也就是渲染畫面, 都是依賴canvas。 所以view 上都makeCanvas 例項的方法,後面所有的動作,都在這個canvas 上去渲染, 從而形成動畫。

2d 其實是一種最簡單 也是最常用的方法, 但是canvas 不僅僅只有 2d 還有webgl 和 webgl2。

其實也就是說白了 我們這個東西后面擴充套件 3d 能力 是OK的 ,其實我也做了一些嘗試, 但是由於業務場景 不是特別多,也是自己展示。

我這裡可以給大家分享一下我用我們動效工廠 3D 做出的動畫。

好了言歸正傳我們繼續寫我們的粒子action

我們看下這張圖:

一個粒子在固定的view 移動,如果移動邊界,其實也就是這時候速度該怎麼變化呢,其實也就是所謂的碰撞檢測

我這裡其實就是有一個彈力系數, 當粒子運動到邊界 也就是粒子的vx 方向 和 vy 方向 變成相反方向,如果沒有開啟碰撞檢測的話, 其實就是去改變 粒子的位置, 設定粒子的 x 和 y 值 在對應的位置。看下面這張圖

我這只是一個方向的。 程式碼如下:

// 碰撞檢測

 checkWall(isWallCollisionTest = true, bounce = 1, W = 1000, H = 1000) { 
        if (isWallCollisionTest) { 
            if (this.x + this.radius > W) { 
                this.x = W - this.radius; 
                this.vx *= -bounce; 
            } else if (this.x - this.radius < 0) { 
                this.x = this.radius; 
                this.vx *= -bounce; 
            } 
 
            if (this.y + this.radius > H) { 
                this.y = H - this.radius; 
                this.vy *= -bounce; 
            } else if (this.y - this.radius < 0) { 
                this.y = this.radius; 
                this.vy *= -bounce; 
            } 
        } else { 
            if (this.x - this.radius > W) { 
                this.x = 0; 
            } else if (this.x + this.radius < 0) { 
                this.x = W; 
            } 
            if (this.y - this.radius > H) { 
                this.y = 0; 
            } else if (this.y + this.radius < 0) { 
                this.y = H; 
            } 
        } 
    } 

我們我設定100 的粒子 我們看下 碰撞檢測的效果:

這裡的卡頓是錄製視訊的時候卡頓和我們其他沒關係的。canvas 粒子數量不夠多的時候是不會卡頓的。

文字粒子

然後我們在當前的粒子上進行了擴充,擴充下文字粒子。這裡其實簡單說下,canvas整個畫布其實就是畫素點,我們可以實現原理其實很簡單,Canvas中有個getImageData的方法,可以得到一個矩形範圍所有畫素點資料。那麼我們就試試來獲取一個文字的形狀吧。

data屬性返回一個 Uint8ClampedArray,它可以被使用作為檢視初始畫素資料。每個畫素用4個1bytes值(按照紅,綠,藍和透明值的順序; 這就是"RGBA"格式) 來代表。每個顏色值部份用0至255來代表。每個部份被分配到一個在陣列內連續的索引,左上角畫素的紅色部份在陣列的索引0位置。畫素從左到右被處理,然後往下,遍歷整個陣列

我這裡使用的畫布大小是 1000 * 1000, 用座標系來表示就是x軸1000,y軸1000

其實就是RGBA(255,255,255,0) 這四個類似的數字表示一個畫素,那10001000這個畫布用Uint8ClampedArray陣列表示,總共由多少個元素呢? 就是1000 1000 * 4 個元素

第一步,用 measureText 的方法來計算出文字適當的尺寸和位置。

 const canvas = document.createElement('canvas')
 const ctx = canvas.getContext('2d');
 canvas.setAttribute('width', 1000);
 canvas.setAttribute('height', 1000);
 ctx.clearRect(0, 0, 1000, 1000)
 ctx.font = 'bold 10p Arial'
 const measure = ctx.measureText(this.text)
 const size = 0.15
 const fSize = Math.min(1000 * size * 10 / 7, 1000 * size * 10 / measure.width);  
 // 10畫素字型行高 lineHeight=7 magic
  ctx.font = `${fSize}px Arial`;
  let left = (1000 - measure.width) / 2 - measure.width / 2;
  const bottom = (1000 + fSize / 10 * 7) / 2;
  ctx.fillText(this.text, left, bottom)
  const imageData = ctx.getImageData(0, 0, 1000, 1000).data
  for (let x = 0; x < 1000; x += 4) {
      for (let y = 0; y < 1000; y += 4) {
          const fontIndex = (x + y * 1000) * 4 + 3;
          if (imageData[fontIndex] > 0) {
              this.points.push({
                  x, y
              })
          }
      }
  }

其實這裡 我們就獲得畫素, 獲得文字所組成的畫素點。然後我們就可以渲染了, 這裡我們看下下面這個視訊:

飛書20220309-111315.gif

為什麼用動效工廠??

這些你都不需要去重新寫,你只要會用我們的動效工廠、簡單配置就可以生成上面的粒子動畫。

在頁面上配置
截圖2022-03-09 上午11.34.13.png

你可以在這個頁面進行配置, 配置當前action對應的屬性,然後這東西 和我們的dsl 有關係, 這個屬性的命名一定要注意。 我會在後面文章進行一步一步剖析。

分析動畫

上圖文字粒子動畫從動畫的角度就是4個關鍵幀, 然後到一步就進行切換。 那其實對應到我們動效工廠其實就是的時間軸是這個樣子的

我第一個關鍵幀對應的動畫引數,以及持續的時間;然後第一幀結束完,播放第二幁。

那麼這樣對應的檢視其實也就是 4個view ,因為檢視和 action一一對應的, 或者是一對多的關係。

也就是根節點 我們的root 下面有4個view如圖:

然後我們選擇任意檢視, 點選+ 號 選擇 粒子彈框如下:

選擇粒子,然後底下進度條就會出現、點選,就可以在動畫屬性編輯。

我們設定文字的引數,文字粒子的大小。

如圖:

其實後面幾個都是同樣的操作,唯一不同的就是展示的文字不同,還有一個就是延遲action執行,第二個延遲1000秒第三個延遲2000以此類推。

當你進度條看下這個樣子你就會明白了,一目瞭然

然後點選播放按鈕就可以預覽動畫了:

飛書20220309-111318.gif

HOW TO USE

這個其實非常簡單,點選左下角的下載按鈕,會生成一個ts檔案

這個ts檔案就是包括當前檢視以及資源view的各種資訊,不過你不需要考慮。

直接安裝我們的sdk

yarn install @wind/core 

然後呼叫

import { play } from '@wind/core' 
play(animation(), {container: canvas 節點}) 

這樣就可以使用了!

最後

大家有問題的可以在評論區交流哦!

文/Fly

關注得物技術,做最潮技術人!

相關文章