原生JS實現DOM爆炸效果
爆炸動效分享
前言
此次分享是一次自我元件開發的總結,還是有很多不足之處,望各位大大多提寶貴意見,互相學習交流。
分享內容介紹
通過原生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 cbof 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方法,用於使用者展示不同的內容進行爆炸:boom:
接下來考慮一下動畫的實現過程,動畫毫無疑問為拋物線動畫,這種動畫在程式碼中實現可以使用物理公式,
但是我們也可以通過速度曲線實現,想想上拋過程可以想成 由於重力影響 ,變成一個速度逐漸減小的向上位移的過程,
而下拋過程可以理解為加速過程;
則可對應為速度曲線的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更好一些,哈哈:smile:
對於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, });
程式碼資源
ofollow,noindex">原始碼連結元件效果預覽

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