本文介绍一个小型动画库anime.js,anime.js 是一款功能强大的Javascript 动画库插件。anime.js 可以和CSS3 属性,SVG,DOM 元素和JS 对象一起工作,制作出各种高性能,平滑过渡的动画效果。
anime.js虽然没有其他动画库功能强大,但是它包含的功完全能够满足日常活动类开发,并且它体积很小,压缩后的anime.min.js只有18kb。下面简单介绍aminie.js提供了哪些动画方法,并举例说明如何在项目中使用。
1. 基本概念
1.1 动画的目标对象
- 可使用任意CSS选择器作为动画目标,不能用伪元素。
anime({
targets: '.css-selector-demo .el',
translateX: 250
})
- 使用DOM节点或节点的集合作为动画目标。
var elements = document.querySelectorAll('.dom-node-demo .el');
anime({
targets: elements,
translateX: 270
});
- 以JavaScript对象作为动画目标,这个对象必须含有至少一个数字属性。这个在vue中非常有用,例如这个数据用在动态样式中,那随着这个样式变化,这样就可以看到一个动画效果。
var battery = {
charged: '0%',
cycles: 120
}
anime({
targets: battery,
charged: '100%',
cycles: 130,
round: 1,
easing: 'linear',
update: function() {
logEl.innerHTML = JSON.stringify(battery);
}
});
- 以数组作为动画目标,以数组形式接受以上三种类型的对象。
var el = document.querySelector('.mixed-array-demo .el-01');
anime({
targets: [el, '.mixed-array-demo .el-02', '.mixed-array-demo .el-03'],
translateX: 250
});
1.2 可动画的目标属性
大多数CSS属性都会导致布局更改或重新绘制,并会导致动画不稳定。 因此尽可能优先考虑opacity和CSS transforms,这两个属性不会触发重绘和重排。
- 支持常见值是数值的css属性,例如width,top,margin等。
- 支持相对数值,例如在原来基础上增加,减少一个数字,乘以一个数字等,举例如下
var relativeEl = document.querySelector('.el.relative-values');
relativeEl.style.transform = 'translateX(100px)';
anime({
targets: '.el.relative-values',
translateX: {
value: '*=2.5', // 100px * 2.5 = '250px'
duration: 1000
},
width: {
value: '-=20px', // 28 - 20 = '8px'
duration: 1800,
easing: 'easeInOutSine'
},
rotate: {
value: '+=2turn', // 0 * 2 = '2turn'
duration: 1800,
easing: 'easeInOutSine'
},
direction: 'alternate'
});
- 支持颜色动画,单位可以是Haxadecimal,RGB,RGBA,HSL,HSLA
1.3 时间轴(Timeline)
时间轴可让你将多个动画同步在一起。默认情况下,添加到时间轴的每个动画都会在上一个动画结束时开始。这样就可以连续播放多个动画,在实际开发中经常会遇到多个动画先后播放的场合,用这个时间轴的功能就可以轻松解决。看下面的例子:
// 使用默认参数创建时间轴
var tl = anime.timeline({
easing: 'easeOutExpo',
duration: 750
});
// 增加子项
tl
.add({
targets: '.basic-timeline-demo .el.square',
translateX: 250,
})
.add({
targets: '.basic-timeline-demo .el.circle',
translateX: 250,
})
.add({
targets: '.basic-timeline-demo .el.triangle',
translateX: 250,
});
这里只介绍几个重要的概念,anime.js提供了丰富的api,其他可以参考官方文档。
2. 红包雨动画
下面我们来介绍如何使用anime.js实现一个红包雨动画,这里不仅使用到anime.js动画,还用到lottie动画。关于lottie动画这里不做详细介绍,这个动画是点击到红包的时候显示一个爆炸的效果,起到一个点缀(模拟烟花爆炸)的作用。我们先整体看看这个动画有哪些元素和交互组成。
2.1 需求分解
2.1.1 三二一倒计时
动画开始是一个倒计时,从3倒数到1时显示红包降落动画,这个倒计时也是动画的一部分,UI给到的蓝湖如下图1
![抽奖动画 - 红包雨抽奖_第1张图片](http://img.e-com-net.com/image/info8/aac0711c5c054030824d0933c51c1463.jpg)
图1
2.1.2 红包降落
开始动画的时候要显示另外一个倒计时,这个倒计时是限制抢红包的时间是8秒,在这个时间范围内用户可以点击降落的红包,这里产品要求8秒内
红包持续降落,后端给到一个随机数,例如3,在用户点到第3个红包的时候请求抽奖接口,获取抽奖结果。如果用户在8秒结束时点击次数小于这个随机数,或者用户根本就没有点也会请求,接口在这种情况下接口返回的结果是错过机会。UI给到的高保如下图2:
![抽奖动画 - 红包雨抽奖_第2张图片](http://img.e-com-net.com/image/info8/66f25bcb2f574d2eb13c16bc379cdc9c.jpg)
图2
从高保上看,这里涉及到的动画有:
-
倒计时,从8变成0;
-
进度条,从左到右填充满;
-
红包降落;
另外根据产品的口头描述,还有个lottery动画
-
用户点中红包,红包爆炸,变成烟花,红包消失;
2.1.3 中奖弹窗
根据请求接口的结果,显示中奖结果,这个就相对简单,高保图如下:
![抽奖动画 - 红包雨抽奖_第3张图片](http://img.e-com-net.com/image/info8/7e371127389644a08fec24c856e159ab.jpg)
图3
注意点击继续抢红包的时候,重新开始第二次抽奖,直至没有剩余抽奖机会,底部按钮会显示查看奖励。如果开始第二次抽奖,要把上次播放的动画复原到初始状态,重新开始。
2.2 实现过程
下面我们把这个动画分解成几个部分,逐步分解说明如何实现这个功能。
2.2.1 生成红包
红包
图2中背景上的图片是分开给的,UI给到6张图片的图片命名为raindrop-0.png,raindrop-1.png,等等,如下图3
![抽奖动画 - 红包雨抽奖_第4张图片](http://img.e-com-net.com/image/info8/936ce4eab2eb499ab4c36ffcdcf60384.png)
图4
随机倾斜
并且按照高保上看,图片还是有写倾斜的,可以使用css中的transform: rotateZ(90deg),所以还要给红包图片一个倾斜度,但是每个红包的倾斜度不能相同,需要随机,这样看起来才像“红包雨”。这个用到了一个生成随机数函数来生成倾斜度,如下:
//生成两个整数中间的随机数
export function getRandomIntInclusive(min, max) {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min //含最大值,含最小值
}
传入两个整数,第一个最小数,第二个最大数,返回大于等于最小数,小于等于最大数的随机数。
红包倾斜的角度需要在一个范围之间,并且有两个范围,10deg60deg和120deg160deg之间,这样每个都有倾斜。这里忽略60deg~120deg之间的随机角度,是应为这个区间倾斜的话,看上去太正,例如,90deg是竖直的,如下图示:
![抽奖动画 - 红包雨抽奖_第5张图片](http://img.e-com-net.com/image/info8/a4709c9473714cbabe4735dcd5565fc4.png)
图5
如何选择上面10deg60deb和120deg160deg呢?还是使用随机数,不过这里简单的使用Math.random()方法来控制。注意Math.random()返回值的返回是0到1,所以和0.5比较,要么左偏,要么右偏,不会你出现竖直的情况。如下:
Math.random() > 0.5 ? getRandomIntInclusive(10, 60) : getRandomIntInclusive(120, 170)
2.2.2 图片尺寸
UI给到了6张红包图片raindrop-0.png~raindrop-5.png,红包雨要降落的红包肯定是大于5张的,不然看上去太少了,也不像“雨”,这就有个问题了,这5张红包图片的尺寸不一致,我们需要设置每个图片的尺寸,这里要用到求余计算,“总红包个数 % 6”,这样得到的结果永远都是[0~5],然后我们把图片的尺寸记在一个有6个元素的数组中,如下:
export const pSize = [
{w: 136/7.5, h: 134/7.5},
{w: 170/7.5, h: 202/7.5},
{w: 170/7.5, h: 202/7.5},
{w: 152/7.5, h: 180/7.5},
{w: 152/7.5, h: 180/7.5},
{w: 106/7.5, h: 144/7.5}
]
注意这里除以7.5使用来吧px转换成vw尺寸。
2.2.3 初始位
三二一倒计结束的时刻红包是看不见的,这样红包初始位置要在屏幕之外,这里用到relative/absolute绝对定位,这里用到top: -96。还有个问题,left就不好用一个固定数值了,这里又也需要用到随机数,让红包在x轴随机分布,这样做也是为了让动画看起来像“雨”。代码如下:
getRandomIntInclusive(0, 100 - 170 / 7.5)
注意这里除以7.5使用来吧px转换成vw尺寸。
2.2.4 红包数组
最后的生成红包数组的代码如下:
this.envelop = Array(20).fill({}).map((a, i) => {
let index = i % 6, {w, h} = pSize[index] //尺寸
let obj = {left: 0, top: -96, rotateZ: 0, imgSrc: '', w, h} //top: -96 初始隐藏
obj.rotateZ = Math.random() > 0.5 ? getRandomIntInclusive(10, 60) : getRandomIntInclusive(120, 170) //sui随机倾斜
obj.left = getRandomIntInclusive(0, 100 - 170 / 7.5) //left
obj.imgSrc = require('./../assets/images/red-rain/raindrop-'+ index +'.png') //红包图片
return obj
})
2.2.5 倒计时
三二一倒计时,这里使用setInterval方法,每秒start递减直至为0,页面上用这个start作为数字图片的一部分,在倒计时结束后显示红包雨弹框并开始播放动画,代码如下:
countDownTip() {
//321开始
this.intId = setInterval(() => {
this.countDown.start--
this.$nextTick(() => {
if (this.countDown.start <= 0) {
clearInterval(this.intId)
//3秒后显示红包雨动画
this.isShow.countDown = false
this.playAnime() //播放动画
}
})
}, 1000)
}
2.2.6 进度条&倒计时&红包降落&未点击抽奖
虽然进度条动画,倒计时动画,红包降落动画是同步进行的,这里我们为了代码方便还是用到时间轴Timeline来组织代码。进度条动画是在8秒时间内从左到右铺满,倒计时动画是数字从8逐步减少到0,红包降落动画是修改元素的top属性,从-96(隐藏)到整个屏幕的高度,就是落到屏幕最底部隐藏,注意红包降落的过程中不能所有的一起降落,要有时间上的交错,这里用到交错动画,来看下面的代码。
playAnime() {
this.tl = anime.timeline({easing: 'linear', duration: 8000})
let height = window.screen.height
this.tl.add({ //倒计时动画
targets: this.countDown, //动画目标countDown对象中的rob属性,从8变成0
rob: 0,
duration: 8000, //持续8秒钟
round: 1,
delay: 500,
easing: 'linear',
complete: () => {
this.tl.pause() //结束后动画结束
//8秒后未点击或点击数小于随机数,去抽奖
if (this.btnClickCount < this.chance.random) {
this.lottery()
}
}
}).add({ //进度条动画
targets: '#processImg', //动画目标是标签,css选择器
width: '100%', //修改标签的宽度
duration: 8000 //初始时间是8秒
}, 0).add({ //红包降落动画
targets: '.envelop', //动画目标是标签,一系列div标签
delay: anime.stagger(300, {start: 100}), //交错动画,延迟从100ms开始,然后每个元素增加300ms
easing: 'linear',
top: height, //修改高度
loop: true
}, 0)
}
来看看这个动画的效果,如下图6
图6
从界面效果上看符合需求的预期,右上角倒计时,进度条从左到右铺满,红包持续降落,而不是一起降落。Math.random()和getRandomIntInclusive()方法配合让红包随机左右倾斜并且在x轴随机分布,这样红包看起来更像是一场“雨”。
2.2.7 红包爆炸
在红包降落的过程中,8秒时间内,如果用户点击了红包,会有一个红包爆炸的效果,这里用到Lottie动画。Lottie动画是由专门的动画设计师做好之后发个前端开发人员来接入的,这里我们不做详细介绍,只说一个问题。
Lottery动画设计师输出的产物是动画资源,包含一个img文件夹,里面是图片文件,还有一个data.json数据,引入Lottie插件之后,要额外再引入这个json数据,注意这个json数据里会引入images文件夹下的图片文件,在json对象的assets节点下面。如下图7
![抽奖动画 - 红包雨抽奖_第6张图片](http://img.e-com-net.com/image/info8/2266cfaf47b74dfa97e5548422a00f02.png)
图7
我们看到assest目录下有个图片img_0.png,如下图8
![抽奖动画 - 红包雨抽奖_第7张图片](http://img.e-com-net.com/image/info8/767e8efc175c4cf6b14ce9a17d98deee.jpg)
引入data.json之后要对assets节点下的图片目录特殊处理,使用require()方法引入,不然打包之后找不到图片,如下处理
引入资源数据
import animeData from './../assets/boom/data.json'
处理数据
mounted() {
this.processData()
}
//处理json图片路径
processData() {
shuffle(this.envelop)
animeData.assets.forEach(item => {
item.u = ''
if (item.w && item.h) {
item.p = require(`@/assets/boom/images/${item.p}`) //require处理图片路径
}
})
}
还要安装并引入Lottie插件,如下:
import lottie from 'lottie-web'
点击红包之后要播放当前点击的红包的爆炸动画,并且停止红包雨,代码如下:
//点击红包
btnRob(el, data) {
if (checkLogin()) {
//点击次数加1
this.btnClickCount++
el.target.style.background = 'none' //隐藏红包
let lott = lottie.loadAnimation({
container: el.target,
animType: 'html',
renderer: 'svg',
loop: false,
autoplay: true,
animationData: animeData,
})
lott.setSpeed(3.5)//修改爆炸烟花速度
lott.addEventListener('complete', e => {
setTimeout(() => {
el.target.innerText = '' //隐藏红包
}, 500)
})
//点击次数大于等于随机次数
if (this.btnClickCount >= this.chance.random) {
//停止飘落
this.tl.pause()
//去抽奖
this.lottery()
}
}
}
下面来看看这个爆炸的效果,如下图9
图6
从图中爆炸效果来看,Lottie动画是给这个烟花图片做了一个从小变大的效果。
2.2.8 抽奖
根据需求,在8秒内用户点击红包达到规定次数的时候,去抽奖,没有点击或者点击次数小于规定次数,也会去调抽奖接口,接口会将抽奖机会减1并告诉用户错失机会。来看下面的代码:
//抽奖
lottery() {
this.$toast.loading({message: '加载中...', duration: 0, forbidClick: true, loadingType: 'spinner'})
let {auth} = getLocalStorage()
let data = {
actId: configData.actId,
clickNum: this.btnClickCount,
provinceId: auth.provinceCode,
channelId: configData.channelId
}
api.coc2.redEnvelope.raffle(data).then(res => {
this.$toast.clear()
this.prize = {}
this.$nextTick(() => {
if ([0, 9300001, 8000007, 9300003].includes(res.hRet)) {
if (res.hRet == 0) {
this.prize = res.data
}
this.prize.hRet = res.hRet
this.prize.page = 'red-envelope'
//业务推荐
if (4 === this.prize.prizeType) {
this.$refs.refService && this.$refs.refService.popUp()
}
//福卡
else if (6 === this.prize.prizeType) {
this.$refs.refAlipayCard && this.$refs.refAlipayCard.popUp()
}
//卡券奖励
else {
this.$refs.refWinPrize && this.$refs.refWinPrize.popUp(this.prize)
}
} else if (res.hRet === 303) {
pullLogin()
} else {
this.$toast(res.retMsg)
this.close(true)
EventBus.$emit(EventKey.checkPrize)
}
})
}).catch(e => {
this.$toast.clear()
this.prize.hRet = 8000007
this.$nextTick(() => {
this.$refs.refWinPrize && this.$refs.refWinPrize.popUp(this.prize)
})
})
}
2.2.9 动画复原
上面代码是调接口和接口处理逻辑,和动画关系不大,但是有一个要注意的地方,调接口之后弹出抽奖结果弹框,可能用户还有抽奖机会,这时又可以抽,需要将动画复原。这里有个问题,如果是通过动画修改过的data值,需要重新赋值,并且使用anime.js赋值,直接使用vue中的this.xxx = yyy不起作用,这个估计是修改动画的值的时候没有触发set导致的,来看下面的代码。
close(closeAll) {
this.btnClickCount = 0 //用户点击次数初始化
this.countDown.start = 3
this.countDown.rob = 8
if (closeAll) {
this.isShow.pop = false //关闭整个红包雨弹框
}
this.isShow.countDown = true
clearInterval(this.intId)
this.tl = anime.timeline()
this.tl.add({
targets: '.envelop',
top: -96,
duration: 100,
easing: 'linear'
}).add({
targets: '#processImg',
width: '0%',
duration: 100
})
}
3 最终效果
![抽奖动画 - 红包雨抽奖_第8张图片](http://img.e-com-net.com/image/info8/63bb13a0a42f4622b6178c797758d3d8.gif)
图7
5.参考
- animejs https://www.animejs.cn/
- Lottie https://airbnb.design/lottie/#get-started