【CSS】398- 原生JS實現DOM爆炸效果

  • 2019 年 11 月 5 日
  • 筆記

爆炸動效分享

前言

此次分享是一次自我組件開發的總結,還是有很多不足之處,望各位大大多提寶貴意見,互相學習交流。

分享內容介紹

通過原生js程式碼,實現粒子爆炸效果組件 組件開發過程中,使用到了公司內部十分高效的工程化環境,特此打個廣告:新浪移動誠招各種技術大大!可以私聊投簡歷哦!

效果預覽

效果分析

  • * 點擊作為動畫開始的起點,自動結束
  • * 每次效果產生多個拋物線粒子運動的元素,方向隨機,展示內容不一樣,有空間上Z軸的大小變化
  • * 需求上可以無間隔點擊,即第一組動畫未結束可播放第二組動畫
  • * 動畫基本執行時長一致

由以上四點分析後,動畫實現有哪些實現方案呢?

  • css操作態變換(如focus)使子元素執行動畫

`不可取,效果可多次連點,css狀態變換與需求不符`

  • Js 控制動畫開始,事先寫好css動畫預置,通過class 包含選擇器切換動畫 例如:.active .items{animation:xxx …;}

`不可取,單次執行動畫沒有問題,但是存在效果的固定,以及無法連續執行動畫`

  • 事先寫好大量動畫,隱藏大量dom元素,動畫開始隨機選取dom元素執行自己唯一的動畫keyframes

`實現層面來說,行得通,但是評論列表長的時候,dom數量巨大,且css大量動畫造成程式碼量沉重、無隨機性`

  • 拋棄css動畫,使用canvas 繪製動畫

`可行,但是canvas維護成本略高,且自定義功能難設計,螢幕適配也有一定成本`

  • js做dom創建,生成隨機css @keyframes

`可行,但是創建style樣式表,引發css重新渲染頁面,會導致頁面的性能下降,且拋物線css的複雜度不低,暫不作為首選`

  • js 刷幀 做dom渲染

`可行,但是刷幀操作會造成性能壓力`

結論

canvas雖說可行,但由於其開發弊端 本次分享不以canvas為分享內容,而是使用最後一種 js刷幀的dom操作

組件結構

由截圖分享,動畫可以分為兩個模組,首先,隨機發散的粒子具有共性:拋物線動畫,淡出,渲染表情

而例子數量變多之後則為截圖中的效果

但是,由於性能原因,我們需要做到粒子的掌控,實現資源再利用,那麼還需要第二個模組,作為粒子的管控組件

所以:此功能可使用兩個模組進行開發:partical.js 粒子功能 與 boom.js 粒子管理

實現 Partical.js

1. 前置資源:拋物線運動的物理曲線需要使用Tween.js提供的速度函數

若不想引入Tween.js 可以使用以下程式碼

/** Tween.js* t: current time(當前時間);* b: beginning value(初始值);* c: change in value(變化量);* d: duration(持續時間)。* you can visit '緩動函數速查表' to get effect*/          const Quad = {      easeIn: function(t, b, c, d) {          return c * (t /= d) * t + b;      },      easeOut: function(t, b, c, d) {          return -c *(t /= d)*(t-2) + b;      },      easeInOut: function(t, b, c, d) {          if ((t /= d / 2) < 1) return c / 2 * t * t + b;          return -c / 2 * ((--t) * (t-2) - 1) + b;      }}const Linear = function(t, b, c, d) {       return c * t / d + b; }

2. 粒子實現

實現思路:

希望在粒子管控組件時,使用new partical的方式創建粒子,每個粒子存在自己的動畫開始方法,動畫結束回調。

由於評論列表可能存在數量巨大的情況,我們希望只全局創建有限個數的粒子,那麼則提供呢容器移除粒子功能以及容器添加粒子的功能,實現粒子的復用

partical_style.css

//粒子充滿粒子容器,需要容器存在尺寸以及relative定位.Boom-Partical_Holder{      position: absolute;      left:0;      right:0;      top:0;      bottom:0;      margin:auto;}

particle.js

import "partical_style.css";          class Partical{      // dom為裝載動畫元素的容器 用於設置位置等樣式      dom = null;      // 動畫開始時間       StartTime = -1;      // 當前粒子的動畫方向,區別上拋運動與下拋運動      direction = "UP";      // 動畫延遲      delay = 0;      // 三方向位移值      targetZ = 0;      targetY = 0;      targetX = 0;      // 縮放倍率      scaleNum = 1;      // 是否正在執行動畫      animating = false;      // 粒子的父容器,標識此粒子被渲染到那個元素內      parent = null;      // 動畫結束的回調函數列表      animEndCBList = [];      // 粒子渲染的內容容器 slot      con = null;        constructor(){          //創建動畫粒子dom          this.dom = document.createElement("div");          this.dom.classList.add("Boom-Partical_Holder");          this.dom.innerHTML = `            <div class="Boom-Partical_con">                Boom            </div>        `;      }        // 在哪裡渲染      renderIn(parent) {          // dom判斷此處省略          parent.appendChild(this.dom);          this.parent = parent;          // 此處為初始化 slot 容器          !this.con && ( this.con = this.dom.querySelector(".Boom-Partical_con"));       }         // 用於父容器移除當前粒子       deleteEl(){           // dom判斷此處省略           this.parent.removeChild(this.dom);       }         // 執行動畫,需要此粒子執行動畫的角度,動畫的力度,以及延遲時間       animate({ deg, pow, delay } = {}){           // 後續補全       }         // 動畫結束回調存儲       onAnimationEnd(cb) {           if (typeof cb !== 'function') return;           this.animEndCBList.push(cb);       }         // 動畫結束回調執行       emitEndCB() {           this.dom.style.cssText += `;-webkit-transform:translate3d(0,0,0);opacity:1;`;           this.animating = false;           try {               for (let cb  of this.animEndCBList) {                    cb();               }           } catch (error) {               console.warn("回調報錯:",cb);           }       }         // 簡易實現slot功能,向粒子容器內添加元素       insertChild(child){           this.con.innerHTML = '';           this.con.appendChild(child);       }}

致此,我們先創建了一個粒子對象的構造函數,現在考慮一下我們實現了我們的設計思路嗎?

  • * 使用構造函數new Partical( )粒子
  • * 粒子實力對象存在 animate 執行動畫方法
  • * 有動畫結束回調函數的存儲和執行
  • * 設置粒子的父元素: renderIn 方法
  • * 父元素刪除粒子: deleteEl 方法

為了更好的展示粒子內容,我們特意在constructor里創建了一個 Boom-Partical_con 元素用於模擬slot功能: insertChild方法,用於使用者展示不同的內容進行爆炸?

接下來考慮一下動畫的實現過程,動畫毫無疑問為拋物線動畫,這種動畫在程式碼中實現可以使用物理公式,

但是我們也可以通過速度曲線實現,想想上拋過程可以想成 由於重力影響 ,變成一個速度逐漸減小的向上位移的過程,

而下拋過程可以理解為加速過程;

則可對應為速度曲線的easeOut 與 easeIn,

而水平方向可以理解為勻速運動,則是 linear;

我們以水平向右為X正方向0度,順時針方向角度增加;

則 小於 180度為向下, 大於180度為向上

假設方向為`四點鐘`方向,夾角則為 `30` 度,

按照高中物理,大小為N的力:

` 在X軸的分量應為 cos(30) * N ` ` 在Y軸的分量應為 sin(30) * N`

力的分解圖解

也就是說 我們可以知道一個方向上的力在XY軸的分量大小,

假設我們將 力 的概念 轉化為 視圖中 位移的概念,

我們將 力量1 記為 10vh的大小

於是我們可以定義全局變數

const POWER = 10; // 單位 vh 力的單位轉化比例      const G = 5;      // 單位 vh 重力值      const DEG = Math.PI / 180;      const Duration = .4e3; //假設動畫執行時長400毫秒

由此 我們補全 animate方法

 // 執行動畫 角度 , 力 1 ~ 10 ; 1 = 10vh      animate({ deg, pow, delay } = {}) {          this.direction = deg > 180 ? "UP" : "DOWN";          this.delay = delay || 0;          let r = Math.random();          this.targetZ = 0;          this.targetY = Math.round(pow * Math.sin(deg * DEG) * POWER);          this.targetX = Math.round(pow * Math.cos(deg * DEG) * POWER) * (r + 1);          this.scaleNum = (r * 0.8) * (r < 0.5 ? -1 : 1);          this.raf();      }

animte的思路為:通過傳入的角度和力度 計算目標終點位置(因為力最終轉化為位移值,力越大,目標位移越大)

使用隨機數計算此次動畫的縮放值變化範圍(-0.8 ~ 0.8)

然後執行刷幀操作 raf

     raf(){          // 正在執行動畫          this.animating = true;            // 動畫開始時間          this.StartTime = +new Date();          let StartTime = this.StartTime;            // 獲取延時          let delay = this.delay;            // 動畫會在延時後開始,也就是真正開始動畫的時間          let StartTimeAfterDelay = StartTime + delay                let animate = () => {              // 獲取從執行動畫開始經過了多久              let timeGap = +new Date() - StartTimeAfterDelay;              // 大於0 證明過了delay時間              if (timeGap >= 0) {                  // 大於Duration證明過了結束時間                  if (timeGap > Duration) {                      // 執行動畫結束回調                      this.emitEndCB();                      return;                  }                  // 設置應該設置的位置的樣式                  this.dom.style.cssText += `;will-change:transform;-webkit-transform:translate3d(${this.moveX(timeGap)}vh,${this.moveY(timeGap)}vh,0) scale(${this.scale(timeGap)});opacity:${this.opacity(timeGap)};`;              }              requestAnimationFrame(animate);          }          animate();      }

刷幀操作中判斷了delay時間的處理以及結束的時間處理回調

那麼揭曉來就剩下 moveX,moveY,scale,opacity的設置

      // 水平方向為勻速,所以使用Linear      moveX(currentDuration) {          // 此處 * 2 是效果矯正後的處理,可根據自己的需求修改水平位移速度          return Linear(currentDuration, 0, this.targetX, Duration) * 2;      }        // 縮放 使用了easeOut曲線, 可根據需求自行修改      scale(currentDuration) {          return Quad.easeOut(currentDuration, 1, this.scaleNum, Duration);      }        // 透明度 使用了easeIn速度曲線,保證後消失      opacity(currentDuration) {          return Quad.easeIn(currentDuration, 1, -1, Duration);      }        // 豎直方向上位移計算      moveY(currentDuration) {          let direction = this.direction;          if (direction === 'UP') {              // G用於模擬上拋過程的重力              // 如果是上拋運動              if (currentDuration < Duration / 2) {                  // 上拋過程 我們使用easeOut速度逐漸減小,我們讓動畫在一半時移到最高點                  return Quad.easeOut(currentDuration, 0, this.targetY + G, Duration / 2);              }              // 上拋的下降過程,從最高點下降              return this.targetY + G - Quad.easeIn(currentDuration - Duration / 2, 0, this.targetY / 2, Duration / 2);          }          // 下拋運動直接easeIn          return Quad.easeIn(currentDuration, 0, this.targetY, Duration);      }

至此,partical.js 結束,文件末尾加一行

export default Partical; 

此時 我們的partical.js輸出一個構造函數:

  • * new 的時候創建了粒子元素,
  • * 使用onAnimtionEnd可以實現動畫結束的回調函數
  • * insertChild可以向粒子內渲染使用者自定義的dom
  • * renderIn 可以設置粒子父元素
  • * deleteEl 可以從父元素刪除粒子
  • * animate 可以執行刷幀,渲染計算位置,觸發回調

於是對於粒子來說,只剩下在執行animte的時候 傳入的力的大小,方向,以及延遲時間

粒子管理 Boom.js

之所以叫Boom是因為一開始組件名叫Boom,其實叫ParticalController更好一些,哈哈?

對於Boom.js的功能需求為

  • 創建粒子
  • 執行粒子動畫,賦予動畫力、角度、延時
  • 設置粒子容器

可達到效果:

  • 不關心業務,業務使用者傳入每個粒子slot內容數組
  • 粒子組件可復用
  • 易於維護(可能是哈哈哈)

於是粒子管理器構架為:

    import Partical from "partical.js";        class Boom{          // 實例化的粒子列表          particalList = [];          // 單次生成的粒子個數          particalNumbers = 6;          // 執行動畫的間隔時間          boomTimeGap = .1e3;          boomTimer = 0;          // 用戶插入粒子的slot 的內容          childList = [];          // 默認旋轉角度          rotate = 120;          // 默認的粒子發散範圍          spread = 180;          // 默認隨機延遲範圍          delayRange = 100;          // 默認力度          power = 3;          // 此次執行粒子爆炸的是那個容器          con = null;            constructor({ childList , container , boomNumber , rotate , spread , delayRange , power} = {}){                this.childList = childList || [];              this.con = container || null;              this.particalNumbers = boomNumber || 6;              this.rotate = rotate || 120;              this.spread = spread || 180;              this.delayRange = delayRange || 100;              this.power = power || 3;              this.createParticals(this.particalNumbers);          }          setContainer(con){              this.con = con;          }          // 創建粒子 存入記憶體數組中          createParticals(num){              for(let i = 0 ; i < num ; i++){                  let partical = new Partical();                  partical.onAnimationEnd(()=>{                      partical.deleteEl();                  });                  this.particalList.push(partical)              }          }          // 執行動畫          boom(){              // 限制動畫執行間隔              let lastBoomTimer = this.boomTimer;              let now = +new Date();              if(now - lastBoomTimer < this.boomTimeGap){                  // console.warn("點的太快了");                  return;              }              this.boomTimer = now;                  console.warn("粒子總數:" , this.particalList.length)              let boomNums = 0;              // 在記憶體列表找,查找沒有執行動畫的粒子              let unAnimateList = this.particalList.filter(partical => partical.animating == false);                let childList = this.childList;              let childListLength = childList.length;                let rotate = this.rotate;              let spread = this.spread;              let delayRange = this.delayRange;              let power = this.power;                // 每有一個未執行動畫的粒子,執行一次動畫              for(let partical of unAnimateList){                  if(boomNums >= this.particalNumbers) return ;                    boomNums++;                  let r = Math.random();                  // 設置粒子父容器                  partical.renderIn(this.con);                  // 隨機選擇粒子的slot內容                  partical.insertChild(childList[Math.floor(r * childListLength)].cloneNode(true));                  // 執行動畫,在輸入範圍內隨機角度、力度、延遲                  partical.animate({                      deg: (r * spread + rotate) % 360,                      pow: r * power + 1,                      delay: r * delayRange,                  });              }              // 如果粒子樹木不夠,則再次創建,防止下次不夠用              if(boomNums < this.particalNumbers){                  this.createParticals(this.particalNumbers - boomNums);              }          }      }          export default Boom;

使用demo

 let boomChildList = [];          for(let i = 0 ; i < 10; i++){          let tempDom = document.createElement("div");          tempDom.className = "demoDom";          tempDom.innerHTML = i;          boomChildList.push(tempDom);      }        let boom = new Boom({          childList: boomChildList,          boomNumber: 6,          rotate: 0,          spread: 360,          delayRange: 100,          power: 3,      });

程式碼資源(源碼鏈接)

https://pan.baidu.com/s/1nG35lkj0mYWqalwrl2f7og?errno=0&errmsg=Auth%20Login%20Sucess&&bduss=&ssnerror=0&traceid=#list/path=%2F

組件效果預覽

結尾

可能效果中實現的思維還有不妥和欠缺,歡迎各位大大提出寶貴意見,互相交流、學習!