原生JS實現DOM粒子爆炸效果

爆炸動效分享

前言

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

分享內容介紹

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

效果預覽

圖片描述

效果分析

  • 點擊做爲動畫開始的起點,自動結束
  • 每次效果產生多個拋物線粒子運動的元素,方向隨機,展現內容不同,有空間上Z軸的大小變化
  • 需求上能夠無間隔點擊,即第一組動畫未結束可播放第二組動畫
  • 動畫基本執行時長一致
由以上四點分析後,動畫實現有哪些實現方案呢?
  • css操做態變換(如focus)使子元素執行動畫

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

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

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

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

    實現層面來講,行得通,可是評論列表長的時候,dom數量巨大,且css大量動畫形成代碼量沉重、無隨機性數組

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

    可行,可是canvas維護成本略高,且自定義功能難設計,屏幕適配也有必定成本app

  • js作dom建立,生成隨機css @keyframes

    可行,可是建立style樣式表,引起css從新渲染頁面,會致使頁面的性能降低,且拋物線css的複雜度不低,暫不做爲首選dom

  • 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 'http://easings.net/zh-cn' 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; 
        }
  1. 粒子實現
    實現思路:
    但願在粒子管控組件時,使用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,
});

代碼資源

源碼網盤連接

組件效果預覽

圖片描述

結尾

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

相關文章
相關標籤/搜索