整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

原生JS实现DOM爆炸效果

原生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 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://zhuanlan.zhihu.com/p/47770130

TML5技术正在不断的发展和更新,越来越多的开发者也正在加入HTML5阵营,甚至在移动开发上HTML5的地位也是越来越重要了。HTML5中的大部分动画都是通过Canvas实现,因为Canvas就像一块画布,我们可以通过调用脚本在Canvas上绘制任意形状,甚至是制作动画。本文就是收集了很多非常富有创意的一些canvas动画特效例子,这些例子都非常适合大家学习。

1、HTML5 Canvas高空瀑布下落湖面动画

HTML5 Canvas是一个神奇的网页技术,我们在Canvas画布上可以做任何有趣的事情。今天要分享的这款瀑布动画就是利用了HTML5 Canvas的相关特性实现的。记得我们在很早以前给大家介绍过一个超逼真的HTML5瀑布动画,也是在Canvas上完成的,非常酷。今天的这个瀑布更加美妙,因为它模拟了整个瀑布落入湖面的美妙景象,但是从逼真度上来说,还是稍微有待改进,不过个人觉得已经非常不错了。

2、HTML5/CSS3 3D雷达扫描动画

之前我们分享过一款纯CSS3雷达扫描模拟动画,看起来十分炫酷。这次我们分享的另外一款雷达动画更加让人震撼,它是基于HTML5和CSS3实现,它的一大特点是3D立体的视觉效果,鼠标点击雷达后将会展现一张3D立体地图,并且对地图上指定的几个地点进行坐标详细信息描述。

3、HTML5 Canvas 图片粒子沙漏动画

之前我们分享过很多款炫酷的HTML5 Canvas粒子动画,比如这款HTML5 Canvas 多种炫酷3D粒子图形动画和HTML5 Canvas文字粒子动画就都非常不错。这次我们要给大家带来的是一款基于HTML5 Canvas的图片粒子沙漏动画,主要是将一张图片打散成粒子,然后模拟沙漏将图片粒子掉落下来。

4、HTML5 Canvas火焰文字动画特效

HTML5技术确实挺强大的,特别是Canvas画布更是让网页动画变得丰富多彩。今天我们分享的是一款基于HTML5 Canvas的火焰文字动画特效,它可以让任意文字上方冒出密集的火焰,就像这些文字在熊熊燃烧一样。与这款火焰动画类似的还有以前分享的HTML5 Canvas幻彩火焰文字特效。

5、HTML5 WebGL粒子爆炸动画

之前我们分享过几款非常炫酷的HTML5粒子动画,比如这款HTML5像素粉碎图片动画和HTML5 Canvas彩色像素进度条动画,都是利用了HTML5的粒子渲染特性实现。今天我们要分享另外一款基于HTML5和WebGL的粒子爆炸动画特效,效果非常令人震撼。

6、超炫酷HTML5 Canvas蝴蝶飞舞动画

还记得很早以前我们为大家分享过一款非常炫酷的HTML5蝴蝶3D动画,它是基于HTML5和SVG实现的。这次我们要再一次为大家介绍另外一款同样也很酷的HTML5 Canvas蝴蝶飞舞动画,蝴蝶是在Canvas上绘制而成,利用HTML5的动画特性实现蝴蝶的飞舞,大家可以学习一下。

7、HTML5 Canvas 3D天体运行动画

今天我们要给大家分享一款基于HTML5 Canvas的3D星球天体运行动画,这里我们在Canvas画布上绘制了一颗较大的星球,然后在大星球周围有一圈很小的陨石区域,这些陨石会围绕着星球不停地旋转,而且配合黑色的背景后带有很强烈的3D视觉效果。

8、HTML5 Canvas 房间3D模型动画 可读取麦克风和摄像头

这是一款基于HTML5 Canvas的3D房间模拟动画,房间里有电视机、沙发、书柜、灯具以及一个人物模型,这些模型都是在Canvas上绘制而成。更重要的是,这款3D动画可以利用HTML5特性读取本地麦克风和摄像头,这样就可以通过摄像头将你自己投影到电视机上,看上去挺神奇的。

9、HTML5 Canvas 随机色彩光束爆炸动画特效

今天我们要给大家分享一款非常炫酷的HTML5 Canvas光束爆炸动画特效,它就像一朵光速爆炸开一样,动画效果非常绚丽。点击鼠标时,可以随机切换光速的颜色,当然你也可以在页面上放几个按钮,通过点击按钮来指定某一种颜色的光束。

10、HTML5 Canvas粒子数字时钟动画

还记得我们之前分享的几款HTML5粒子动画特效吗?比如这款HTML5文本输入框粒子动画特效和HTML5 Canvas生成粒子效果的人物头像,效果都非常炫酷。今天我们要给大家介绍的也是一款基于HTML5 Canvas的粒子数字时钟动画,时钟会读取本地时间,并且每变化一次均会出现粒子动画效果,这款粒子数字时钟非常适合在你的个性化博客中使用。

11、HTML5 Canvas 圆形进度条 显示数字百分比

记得以前为大家分享过很多样式各异的进度条插件,有基于jQuery的,也有基于HTML5和CSS3的。这次我们要介绍另外一款基于HTML5 Canvas的圆形进度条应用,在黑色的背景下,白色的进度条显得格外显眼,而且圆形中央会实时显示当前进度的数字百分比,非常实用。

12、HTTML5 Canvas心电图动画 可多参数控制

今天要给大家分享另外一个超炫酷的HTML5 Canvas动画,它是一个心电图动画效果,程序运行时就会模拟心电图在屏幕上打印当前心跳信息。同时动画中带有一些开关按钮来控制心电图中的各个参数,动画相当逼真。

13、HTML5 Canvas 梦幻树生长动画

今天我们要为大家分享一款基于HTML5 Canvas的动画特效,它是一颗逐渐生长的梦幻大树,生长过程中树枝将会产生随机的色彩,让整一棵大树显得非常具有梦幻的效果。本实例利用了HTML5 Canvas的动画技术,实现了渐变式动画的特效。

14、CSS3实现五彩3D旋转星球

之前我们有分享过很多纯CSS3和HTML5实现的球体动画,比如这款HTML5 3D球体斑点运动动画和HTML5 Canvas 地球旋转3D动画都非常不错。今天要给大家分享另外一款超炫酷的CSS3五彩3D旋转星球,旋转起来的视觉效果相当震撼。

15、HTML5 Canvas闪亮的3D蓝宝石动画

几天前,我们向大家分享过一款基于HTML5 Canvas的3D钻石动画,制作得十分逼真。今天我们要分享另外一个基于HTML5 Canvas的3D蓝宝石动画,我们可以通过鼠标的拖拽来实现蓝宝石的各个视角的观察,并且通过鼠标滚轮来缩放宝石的大小,同时蓝宝石的表面会不时地发出闪亮的光芒,不得不说,Canvas在网页绘制方面真的是无所不能。

16、HTML5 Canvas 蓝色3D钻石旋转动画

今天我们要在HTML5 Canvas画布上绘制一颗高贵典雅的蓝色3D钻石,我们在Canvas画布上通过绘制很多个不同大小的多边形组成了一颗钻石,然后通过CSS3的颜色渐变特性让钻石的表面产生白色发光的特效,同样再利用CSS3的动画属性让钻石不停地旋转,展现出3D的视觉效果,非常炫酷。

以上就是16个富有创意的HTML5 Canvas动画特效集合,如果你对HTML5感兴趣,欢迎下载源码学习。

信 8.0 更新的一大特色就是支持动画表情,如果发送的消息只有一个内置的表情图标,这个表情会有一段简单的动画,一些特殊的表情还有全屏特效,例如烟花表情有全屏放烟花的特效,炸弹表情有爆炸动画并且消息和头像也会随之震动。

本着前端工程师的职业精神,我就想看看能不能实现一个类似的特效。折腾许久之后,做出来的效果如下:

项目的核心是使用到了 lottie 动画库。lottie 是 Airbnb 出品的、全平台(Web、Android、IOS、React Native)的动画库,它的特点在于能够直接播放使用 Adobe After Effects 制作的动画。设计师在 After Effects 中,利用 Bodymovin 插件把动画导出为 JSON 格式之后,开发者就能够通过相应平台的 SDK 进行播放。(项目地址及示例演示见文末)

在做完这个项目之后我感觉到自己的前端储备又丰富了一层,在以后应对复杂特效时又有了新的思路,如果你也想进一步提升前端开发技能,可以跟着这篇文章实践一下。本篇文章除了使用 Lottie 库之外,全部都是使用原生 HTML/CSS/JavaScript 实现的,这样无论你是 React、Vue 还是其它工程师,都可以快速掌握。


编写界面



本来想跳过 HTML/CSS 部分,但是想到 CSS 可能是大部分人的弱项,所以我决定还是把实现界面的思路写一下,想看核心部分的可以直接跳到:二、发送普通消息部分。

1. HTML 部分

首先看 HTML 部分,从效果图来看:

上边有一个标题栏,显示与 XXX 聊天。

中间是聊天信息面板,包含着双方发送的消息,每条消息由发送者头像和消息内容组成,我发送的在右侧,对方发送的在左侧。

下方是底部信息,有表情选择按钮、编辑消息文本框和发送按钮。

那么根据这个结构编写的 HTML 代码如下所示:

<main>
  <div class="chat">
    <div class="titleBar">与 XXX 聊天</div>
    <div class="panel">
      <div class="message mine">
        <img src="./me.png" alt="" />
        <p><span>你好</span></p>
      </div>
      <div class="message yours">
        <img class="avatar" src="./you.png" alt="" />
        <p><span>Hi</span></p>
      </div>
      <!-- 省略其它消息 -->
    </div>
    <footer>
      <button class="chooseSticker">
        <img src="./emoji.svg" alt="" />
        <div class="stickers"></div>
      </button>
      <input
             class="messageInput"
             type="text"
             name=""
             id=""
             placeholder="请输入聊天信息"
             />
      <button class="send">发送</button>
    </footer>
  </div>
</main>

各个元素所对应的界面部分为:

<main /> 元素是一个整体的容器,用于把聊天窗口居中对齐

<div class="chat"> 是聊天应用的容器,用于布局标题栏、聊天面板和底部发送框。

<div class="titleBar"> 用于显示标题栏。

<div class="panel"> 是消息面板,用于布局其中的消息。

<div class="message"> 为消息容器,使用不同的 class 来区分发送方, mine 代表我发送的, yours 代表对方发送的。每条消息里边使用 <img class="avatar" > 来展示头像,使用 <p> 元素来显示文本, <p> 元素里边的 <span> 元素将会作为 lottie 的容器来播放表情动画。

<footer> 用于布局底部操作按钮和消息发送框。其中:

<button class="chooseSticker"> 是表情选择按钮,使用一个笑脸 svg 图片表示,里边的 <div class="stickers"> 是表情选择框弹出层,里边的表情将在 JS 中动态加载,目的是为了实现动画预览。

<input class="messageInput" /> 是聊天消息输入框,没什么特别的。

<button class="send"> 是发送按钮

这个是 HTML 的基本结构,接下来看一下 CSS 样式。

2. CSS 部分

在项目根目录下创建一个 style.css 文件并在 index.html 的<head> 标签中引入:

<link rel="stylesheet" href="style.css" />

2.1 全局样式

首先定义一些 CSS 变量,CSS 变量是为了方便我们引用同一属性值的,后边如果更新样式时,可以避免多次修改:

:root {
  --primary-color: hsl(200, 100%, 48%);
  --inverse-color: hsl(310, 90%, 60%);
  --shadow-large: 0 0px 24px hsl(0, 0%, 0%, 0.2);
  --shadow-medium: 0 0 12px hsl(0, 0%, 0%, 0.1);
}

这些变量的含义分别是:

--primary-color: hsl(200, 100%, 48%) ,主色调,例如我发送的消息的蓝色背景。

--inverse-color: hsl(310, 90%, 60%) ,反色调,或强调色调,与主色调形成鲜明对比,例如发送按钮的背景色。

--shadow-large: 0 0px 24px hsl(0, 0%, 0%, 0.2) ,大阴影,例如标题栏、底部栏的阴影。

--shadow-medium: 0 0 12px hsl(0, 0%, 0%, 0.1) ,小阴影,例如输入框和表情选择弹出层。

接下来是一些重置样式:

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
  font-family: Helvetica, "PingFang SC", "Microsoft Yahei", sans-serif;
}

这些样式对所有元素都有效,设置盒子模型为 border-box ,这样内边距、边框都算在宽高之内,设置内间距和外间距为 0,最后设置默认字体。

2.2 Main 容器

Main 容器用于定位聊天应用容器到浏览器中间,使用 grid 布局,宽高分别设置为浏览器可视区域的 100%,并把背景色设置为黑灰色:

main {
  display: grid;
  place-items: center;
  width: 100vw;
  height: 100vh;
  background-color: hsl(0, 0%, 10%);
}

2.3 聊天应用容器

聊天应用容器设置了固定宽高,模拟手机屏幕,并使用 grid 布局来控制标题栏、聊天面板和底部操作栏的位置:

.chat {
  width: 375px;
  height: 700px;
  background: hsl(0, 0%, 100%);
  border-radius: 8px;
  display: grid;
  grid-template-rows: max-content 1fr max-content;
}

这里使用了 grid-template-rows 把聊天应用分成了 3 行,第一行的标题栏和最后一行的标底部操作栏的高度分别为内容的最大高度,中间的聊天面板则是浮动高度。

2.4 标题栏

标题栏简单的设置了一个内间距、文字居中方式和阴影:

.titleBar {
  padding: 24px 0;
  text-align: center;
  box-shadow: var(--shadow-large);
}

界面优化提示:内间距用来增加留白,在视觉上引起放松,阴影则为了和下边的聊天面板区分开

2.5 聊天面板

聊天面板使用 flex 布局对其中的消息进行排列,并设置方向为按列排布,然后设置 overflow 为 auto,在消息整体高度超出面板高度时,出现滚动条:

.panel {
  display: flex;
  flex-direction: column;
  padding: 24px 12px;
  overflow: auto;
}

界面优化提示:这里的 padding 同样是为了留出足够多的空白,来与其它元素隔开一段距离,以避免拥挤感。

2.6 消息

消息分为消息容器、头像和消息体 3 个部分。其中消息体和头像包含在消息容器中,先来看消息容器的样式。消息容器使用 flex 布局来把消息体和头像放在一行,宽度最大为面板宽度的 80%,并设置字体和外边距:

.message {
  display: flex;
  max-width: 80%;
  font-size: 14px;
  margin: 8px 0;
  position: relative;
}

这里的 position 设置为 relative 是为了定位后边的全屏特效动画。

头像简单设置了宽高、圆角和距离消息体的间距:

.message img {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  margin-right: 12px;
}

界面优化提示:这里不得不再提一下间距的重要性,一定不要把各个元素安排的太过紧凑,否则十分影响视觉效果,最直接的影响就是引起视觉上的拥挤感,造成视觉疲劳。

消息体同样的设置了间距和圆角,这里的圆角和头像的保持一致,以增加和谐感。它还设置了阴影,并使用 flex 布局,把里边的文字或表情消息居中对齐:

.message p {
  padding: 8px 12px;
  border-radius: 12px;
  box-shadow: var(--shadow-large);
  display: flex;
  align-items: center;
}

这些样式默认都是基于对方的消息的,如果是我发送的消息需要放到右边,并作一些调整。首先对于我发送的消息,把 flex-flow 改为 row-reverse 这样头像和消息体的位置就互换了,然后使用 align-self 对齐到面板的右边:

.message.mine {
  flex-flow: row-reverse;
  align-self: flex-end;
}

调整头像的外边距,现在应该是距离左边的消息体的边距了:

.message.mine img {
  margin-right: 0;
  margin-left: 12px;
}

设置消息体的背景色为蓝色,文字为白色:

.message.mine p {
  background-color: var(--primary-color);
  color: white;
}

2.7 底部操作栏

先看底部操作栏容器的整体布局,使用 grid 布局把表情选择按钮、消息发送框和发送按钮分成 3 列,其中除消息发送框为浮动宽度外,其它的两个按钮为固定宽度,默认居中对齐,最后设置阴影和间距:

footer {
  display: grid;
  grid-template-columns: 48px 1fr 75px;
  justify-items: center;
  padding: 12px;
  box-shadow: var(--shadow-large);
}

表情选择按钮把自己进行了靠左对齐,并设置相对定位,用于定位表情选择弹出层,然后设置按钮图标的大小:

.chooseSticker {
  justify-self: start;
  position: relative;
}
.chooseSticker img {
  width: 36px;
  height: 36px;
}

表情选择弹出层的 CSS 代码比较多但都很简单,先看一下代码:

.stickers {
  display: grid;
  grid-template-columns: repeat(auto-fill, 24px);
  column-gap: 18px;

  border-radius: 8px;
  background-color: white;
  box-shadow: var(--shadow-medium);
  padding: 6px 12px;
  font-size: 24px;

  position: absolute;
  top: calc(-100% - 18px);
  width: 300px;
  opacity: 0;
}

这段代码的含义是:

弹出层使用 grid 布局,repeat(auto-fill, 24px) 指的是在宽度允许的条件下,在一行中尽可能放置最多的列元素,每列的宽度固定为 24px。然后设置列间距为 18px。

设置圆角、背景色、阴影、内间距和字体大小。

定位设置为绝对定位,把它向上移动包含元素高度(也就是 .chooseSticker 的高度)的 100% 并减去 18px,调整到合适的位置。宽度设置为 300px,透明度设置为 0 把它隐藏。

消息输入框和按钮的样式比较简单,消息输入框的宽度占满整列,发送按钮使用 justify-self: end 把自己进行靠右对齐。这里把代码一次性贴出来:

.messageInput {
  box-shadow: var(--shadow-medium);
  padding: 0px 12px;
  border-radius: 8px;
  width: 100%;
}
.send {
  height: 100%;
  width: 90%;
  border-radius: 8px;
  justify-self: end;
  color: white;
  background-color: var(--inverse-color);
}

最后再添加一个 .show 样式,用于在点击发送表情按钮时,给表情弹出层添加该样式以显示出来:

.show {
  opacity: 1;
}

3. JS 部分

在给聊天界面加上功能之前,先编写一些基础的 JS 代码。在项目根目录创建一个 index.js 文件,并在 index.html 中引用,注意放到 </body> 关闭标签的上方,这样当 HTML DOM 加载完成之后才会执行 js 中的代码,防止找不到元素:

<body>
   <!-- 其它代码省略 -->
   <script src="index.js"></script>
 </body>


在 index.js 中,先获取要操作的 DOM 元素:

const panelEle=document.querySelector(".panel");
const chooseStickerBtn=document.querySelector(".chooseSticker");
const stickersEle=document.querySelector(".stickers");
const msgInputEle=document.querySelector(".messageInput");
const sendBtn=document.querySelector(".send");

其中:

panelEle 是消息面板元素,用于追加新消息。

chooseStickerBtn 是选择表情按钮,点击它会弹出表情选择框。

stickersEle 就是弹出的表情选择框。

msgInputEle 是消息输入框。

sendBtn 为发送按钮。

然后引入 Lottie 的 js 库,可以到示例代码仓库中下载,也可以在 https://cdnjs.com/libraries/bodymovin 中下载 lottie.min.js,下载完成之后放到项目根目录,然后在 index.html 中,在引入 index.js 的上方引入它:

<script src="lottie.min.js"></script>

下载表情动画资源文件,它们都是 json 格式的文件,下载完成之后直接放到项目根目录即可:

南瓜表情:https://lottiefiles.com/43215-pumpkins-sticker-4

炸弹表情:https://lottiefiles.com/3145-bomb

爆炸动画:https://lottiefiles.com/9990-explosion

接下来看一下各部分功能是怎么实现的。


发送普通消息


发送普通消息时,用户在输入框输入完消息之后,点击发送,就会把该条消息追加到消息列表中,并清空输入框中的内容。那么这里先给发送按钮添加点击事件:

sendBtn.addEventListener("click", ()=> {
  const msg=msgInputEle.value;
  if (msg) {
    appendMsg(msg);
  }
});

在事件处理函数中:

判断用户是否输入了消息。

如果输入了就追加到消息列表中。

来看一下 appendMsg() 函数的代码:

function appendMsg(msg, type) {
  // 创建消息元素
  const msgEle=panelEle.appendChild(document.createElement("div"));
  msgEle.classList.add("message", "mine"); // 设置为“我“发送的样式
  msgEle.innerHTML=`
    <img class="avatar" src="./me.png" alt="" />
    <p><span>${msg}</span></p>
  `;
  // 滚动到最新消息
  panelEle.scrollTop=panelEle.scrollHeight;
  msgInputEle.value="";
}

函数接收两个参数,msg 和 type,分别是要追加的消息内容和类型,type 为可选的,不传则认为是普通文本消息,如果传递了 "stickers" 则为表情消息,现在还用不到它。在这个函数中主要做了下面几件事情:

按照消息的 HTML 结构创建一个新的消息元素 msgEle,并追加到消息列表中。

把消息的样式设置为我发送的。

内部的元素分别为头像和文本消息,使用模板字符串的形式赋值给 msgEle 的 innerHTML 属性中,并在 <p> 中使用 msg 变量的值。

最后把滚动条滚动到最新的消息处,并清空输入框中的消息。

这样就可以发送普通的文本消息了。


发送动画表情


在发送动画表情之前,需要先加载动画表情。在 index.js 的最上方先定义表情名称和表情动画文件路径的键值对信息:

const stickers={
  bomb: {
    path: "./3145-bomb.json",
  },
  pumpkin: {
    path: "./43215-pumpkins-sticker-4.json",
  },
};

我们会根据 bombpumkin 这样的 key 来找到对应的动画路径。接着初始化弹出层中的表情以供用户选择:

// 初始化表情面板,也可以在表情选择窗弹出时再初始化
Object.keys(stickers).forEach((key)=> {
  const lottieEle=stickersEle.appendChild(document.createElement("span"));
  // 对每个表情创建 lottie 播放器
  const player=lottie.loadAnimation({
    container: lottieEle,
    renderer: "svg",
    loop: true,
    autoplay: false,
    path: stickers[key].path,
  });
  // 当选择表情时,发送消息,并设置类型为 sticker 表情消息
  lottieEle.addEventListener("click", ()=> {
    appendMsg(key, "sticker");
  });
  // 当鼠标划过时,播放动画预览
  lottieEle.addEventListener("mouseover", ()=> {
    player.play();
  });
  // 当鼠标划过时,停止动画预览
  lottieEle.addEventListener("mouseleave", ()=> {
    player.stop();
  });
});

这里的代码分别作了下边这些操作:

遍历存储表情信息的对象。

创建一个 lottie 的容器,使用 span 元素,因为 lottie 动画的播放器需要挂载到一个具体的 html 元素中。

调用 lottie 的 loadAnimation() 加载动画,它需要传递这样几个参数:

container: 播放器要挂载到的容器。

renderer:可以选择是使用 svg 还是 canvas 渲染动画。

loop: 是否循环播放,由于此处是在表情选择弹出层中预览动画,所以支持循环播放。

autoplay:是否自动播放,这里设置为了否,后边让它在鼠标划过时再播放动画。

path:动画 json 文件路径,直接从对象中获取。

loadAnimation() 会返回 lottie 的实例,把它保存在 player 中。

然后后边则注册了几个事件:

当 lottieEle 也就是表情被点击时,发送表情消息,给 appendMsg() 的 msg 参数设置为表情的 key,type 参数设置为 "sticker"。

当鼠标划过表情时,开始播放动画。

当鼠标划出表情时,停止动画。

接着给发送表情按钮添加事件,点击时,切换表情弹出层的显示状态:

chooseStickerBtn.addEventListener("click", ()=> {
  stickersEle.classList.toggle("show");
});

这时点击发送表情按钮就可以看到表情选择弹出层了。现在还不能发送表情,因为还没在 appendMsg() 函数中处理,现在来修改一下它里边的代码。首先判断:如果是表情消息,则不在消息中的 <p> 元素里添加任何信息:

function appendMsg(msg, type) {
 // ... 
  msgEle.innerHTML=`
    <img class="avatar" src="./me.png" alt="" />
    <p><span>${type==="sticker" ? "" : msg}</span></p>
  `;
}

然后在它的下方,调用 playSticker() 函数来播放动画:

// 处理表情消息,播放相关动画
if (type==="sticker") {
  playSticker(msg, msgEle);
}

playSticker() 函数接收两个参数,一个是表情的 key,一个是消息元素。此时的 msg 变量的内容就是在 lottieEle 点击事件中传递过来的表情 key。函数中的代码如下:

function playSticker(key, msgEle) {
  // 表情消息,创建 lottie 动画
  const lottieEle=msgEle.querySelector("span");
  lottieEle.style.width="40px";
  lottieEle.style.height="40px";
  lottie.loadAnimation({
    container: lottieEle,
    renderer: "svg",
    loop: false,
    autoplay: true,
    path: stickers[key].path,
  });
}

在这个函数里主要做了下边几项操作:

获取消息中的 span 元素,它将作为 lottie 的动画容器。

设置表情动画的宽高为 40px。

使用 lottie 加载动画,并设置循环播放为 false,自动播放为 true,来让表情发送时自动播放动画,且只播放一次。

现在可以发送表情消息了,相关的动画也会自动播放,接下来看一下怎么实现炸弹的全屏动画和对消息元素的晃动效果。


发送带全屏特效的表情


对于这种带全屏特效的表情可以单独进行判断,也可以在保存表情的对象中定义相关的操作,这里为了简单起见,我们单独判断用户是否发送了炸弹表情,然后施加相应特效。

首先在 appendMsg() 函数里,进行判断,如果发送的消息是表情消息,且表情为炸弹,则播放全屏动画并晃动消息:

function appendMsg(msg, type) {
  if (type==="sticker") {
    playSticker(msg, msgEle);
    if (msg==="bomb") {
      // 播放爆炸动画
      setTimeout(()=> {
        playExplosion(msgEle);
      }, 800);
      // 晃动消息列表
      shakeMessages();
    }
  }
}

这里爆炸全屏动画延迟了 800 毫秒之后再执行,目的是在炸弹表情播放到合适的时间时,再播放全屏动画,播放动画使用了 playExplosion() 函数,并传递了消息元素进去。在爆炸全屏动画结束之后,调用 shakeMessages() 来晃动消息。这里先看一下 playExplosion() 函数的代码:

function playExplosion(anchor) {
  const explosionAnimeEle=anchor.appendChild(document.createElement("div"));
  explosionAnimeEle.style.position="absolute";
  explosionAnimeEle.style.width="200px";
  explosionAnimeEle.style.height="100px";
  explosionAnimeEle.style.right=0;
  explosionAnimeEle.style.bottom=0;
  const explosionPlayer=lottie.loadAnimation({
    container: explosionAnimeEle,
    renderer: "svg",
    loop: false,
    autoplay: true,
    path: "./9990-explosion.json",
  });
  explosionPlayer.setSpeed(0.3);
  // 播放完成后,销毁爆炸相关的动画和元素
  explosionPlayer.addEventListener("complete", ()=> {
    explosionPlayer.destroy();
    explosionAnimeEle.remove();
  });
}

playExplosion() 函数接收一个 anchor 锚点,就是说基于哪个位置开始播放全屏动画,由于示例中的动画画幅比较小,所以把它固定在了最新发送的消息的下方,这里爆炸动画的 anchor 就是消息元素,之后函数做了下边的这些操作:

添加全屏动画元素,设置为绝对定位,宽度 200px,高度 100px,放在最新消息元素的右下角。

加载 lottie 动画,不循环、自动播放。

由于原动画速度过快,这里调用 lottie 实例的 setSpeed() 方法,把速度设置为 0.3 倍速。

之后给 lottie 实例设置事件监听:"complete",它会在动画执行完成时触发,里边销毁了 lottie 实例和全屏动画元素。

这样全屏动画的效果就实现了。接下来看消息晃动的代码:

function shakeMessages() {
  [...panelEle.children]
    .reverse()
    .slice(0, 5)
    .forEach((messageEle)=> {
      const avatarEle=messageEle.querySelector("img");
      const msgContentEle=messageEle.querySelector("p");
      avatarEle.classList.remove("shake");
      msgContentEle.classList.remove("shake");
      setTimeout(()=> {
        avatarEle.classList.add("shake");
        msgContentEle.classList.add("shake");
      }, 700);
    });
}

这个函数的操作是:

使用 reverse() 和 slice() 对最新的 5 条消息进行晃动,也可以把 5 改大一点,对更多消息进行晃动。

然后在循环中,分别给头像和消息添加 shake class 执行晃动动画,这个 class 的内容稍后再介绍。

要注意的是,在添加 shake class执行动画前,需要先删除 shake,因为有的消息可能在之前已经晃动过了,例如当连续发了多个炸弹表情时。后边在添加 shake class 时,使用 setTimeout() 延迟了 700 毫秒,目的是在全屏动画执行到一定程度时再晃动消息。

接下来看一下 shake class 的定义,在 style.css 中添加下方代码:

.shake {
  animation: shake 0.8s ease-in-out;
}
@keyframes shake {
  from {
    transform: translate3d(0, 0px, 0px);
  }
  10% {
    transform: translate3d(6px, -6px, 0px);
  }
  20% {
    transform: translate3d(-5px, 5px, 0px);
  }
  30% {
    transform: translate3d(4px, -4px, 0px);
  }
  35% {
    transform: translate3d(-3px, 3px, 0px);
  }
  39% {
    transform: translate3d(2px, -2px, 0px);
  }
  41% {
    transform: translate3d(-1px, 1px, 0px);
  }
  42% {
    transform: translate3d(0px, 0px, 0px) rotate(20deg);
  }
  52% {
    transform: rotate(-15deg);
  }
  60% {
    transform: rotate(8deg);
  }
  65% {
    transform: rotate(-3deg);
  }
  67% {
    transform: rotate(1deg);
  }
  70% {
    transform: rotate(0deg);
  }
  to {
    transform: translate3d(0px, 0px, 0px) rotate(0);
  }
}

.shake 中使用了 shake keyframes 定义的动画,执行时间为 0.8s,动画执行函数为 ease-in-out。Keyframes 里的代码比较多,但是都是很简单的,就是模拟了爆炸时的效果,移动 x 轴和 y 轴的偏移,每次的偏移幅度越来越小,并且越来越快,可以看到百分比的间隔越来越小。在动画进行到 42% 的时候,加了一些旋转动画,这样就有了落地时的震动效果。由于使用 rotate() 旋转时的轴心在元素中间,我们可以把消息气泡的轴心修改一下来实现更真实的效果:

.message p {
  transform-origin: left bottom;
}
.message.mine p {
  transform-origin: right bottom;
}

这里把对方发送的消息的轴心设置在左下角,自己发送的消息则设置在了右下角。

总结

现在,这个模拟微信 8.0 动画表情的功能就实现了。主要就是下边几点:

使用 lottie 库加载并播放动画。

确定全屏动画的位置和播放时机。

消息晃动动画的 CSS 实现。