原生JS实现DOM粒子爆炸效果

52次阅读

共计 7834 个字符,预计需要花费 20 分钟才能阅读完成。

爆炸动效分享
前言
此次分享是一次自我组件开发的总结,还是有很多不足之处,望各位大大多提宝贵意见,互相学习交流。
分享内容介绍
通过原生 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

前置资源:抛物线运动的物理曲线需要使用 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;
}

粒子实现实现思路:希望在粒子管控组件时,使用 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,
});

代码资源
源码网盘链接
组件效果预览

结尾
,可能效果中实现的思维还有不妥和欠缺,欢迎各位大大提出宝贵意见,互相交流、学习!

正文完
 0