这次分享是一次自我组件开发的总结,仍是有不少不足之处,望各位大大多提宝贵意见,互相学习交流。css
经过原生js代码,实现粒子爆炸效果组件
组件开发过程当中,使用到了公司内部十分高效的工程化环境,特此打个广告: 新浪移动诚招各类技术大大!能够私聊投简历哦!
不可取,效果可屡次连点,css状态变换与需求不符
web
不可取,单次执行动画没有问题,可是存在效果的固定,以及没法连续执行动画
canvas
实现层面来讲,行得通,可是评论列表长的时候,dom数量巨大,且css大量动画形成代码量沉重、无随机性
数组
可行,可是canvas维护成本略高,且自定义功能难设计,屏幕适配也有必定成本
app
可行,可是建立style样式表,引起css从新渲染页面,会致使页面的性能降低,且抛物线css的复杂度不低,暂不做为首选
dom
可行,可是刷帧操做会形成性能压力
函数
canvas虽然说可行,但因为其开发弊端 本次分享不以canvas为分享内容,而是使用最后一种 js刷帧的dom操做性能
由截图分享,动画能够分为两个模块,首先,随机发散的粒子具备共性:抛物线动画,淡出,渲染表情学习
而例子数量变多以后则为截图中的效果动画
可是,因为性能缘由,咱们须要作到粒子的掌控,实现资源再利用,那么还须要第二个模块,做为粒子的管控组件
因此: 此功能可以使用两个模块进行开发: partical.js 粒子功能 与 boom.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); } }
致此,咱们先建立了一个粒子对象的构造函数,如今考虑一下咱们实现了咱们的设计思路吗?
为了更好的展现粒子内容,咱们特地在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输出一个构造函数:
因而对于粒子来讲,只剩下在执行animte的时候 传入的力的大小,方向,以及延迟时间
之因此叫Boom是由于一开始组件名叫Boom,其实叫ParticalController更好一些,哈哈😄
对于Boom.js的功能需求为
可达到效果:
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;
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, });
,可能效果中实现的思惟还有不妥和欠缺,欢迎各位大大提出宝贵意见,互相交流、学习!