抽奖动画|抽奖动画 - 红包雨抽奖

本文介绍一个小型动画库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
2.1.2 红包降落
开始动画的时候要显示另外一个倒计时,这个倒计时是限制抢红包的时间是8秒,在这个时间范围内用户可以点击降落的红包,这里产品要求8秒内
红包持续降落,后端给到一个随机数,例如3,在用户点到第3个红包的时候请求抽奖接口,获取抽奖结果。如果用户在8秒结束时点击次数小于这个随机数,或者用户根本就没有点也会请求,接口在这种情况下接口返回的结果是错过机会。UI给到的高保如下图2:
抽奖动画|抽奖动画 - 红包雨抽奖
文章图片
图2
从高保上看,这里涉及到的动画有:
  • 倒计时,从8变成0;
  • 进度条,从左到右填充满;
  • 红包降落;
    另外根据产品的口头描述,还有个lottery动画
  • 用户点中红包,红包爆炸,变成烟花,红包消失;
2.1.3 中奖弹窗
根据请求接口的结果,显示中奖结果,这个就相对简单,高保图如下:
抽奖动画|抽奖动画 - 红包雨抽奖
文章图片
图3
注意点击继续抢红包的时候,重新开始第二次抽奖,直至没有剩余抽奖机会,底部按钮会显示查看奖励。如果开始第二次抽奖,要把上次播放的动画复原到初始状态,重新开始。
2.2 实现过程 下面我们把这个动画分解成几个部分,逐步分解说明如何实现这个功能。
2.2.1 生成红包
红包
图2中背景上的图片是分开给的,UI给到6张图片的图片命名为raindrop-0.png,raindrop-1.png,等等,如下图3
抽奖动画|抽奖动画 - 红包雨抽奖
文章图片
图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
如何选择上面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 = https://www.it610.com/article/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
抽奖动画|抽奖动画 - 红包雨抽奖
文章图片
图7
我们看到assest目录下有个图片img_0.png,如下图8
抽奖动画|抽奖动画 - 红包雨抽奖
文章图片
引入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 = https://www.it610.com/article/{ 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 最终效果 抽奖动画|抽奖动画 - 红包雨抽奖
文章图片
【抽奖动画|抽奖动画 - 红包雨抽奖】图7
5.参考
  1. animejs https://www.animejs.cn/
  2. Lottie https://airbnb.design/lottie/#get-started

    推荐阅读