• 抽奖动画 红包雨抽奖


    本文介绍一个小型动画库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
        },
         {
          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 = 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)
      }
    
    <mask-slot :is-show="isShow.countDown">
      <div class="content tip">
        <img
          style="margin-top: 30%"
          :src="require('../assets/images/red-rain/count-'+ countDown.start +'.png')"
          class="number"
          alt="" />
      </div>
    </mask-slot>
    

    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选择器
         '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 = {
        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导致的,来看下面的代码。

    <!-- 卡券 -->
    <win-prize ref="refWinPrize" :prize="prize" :chance="chance" @continueRob="continueRob"></win-prize>
    <!-- 业务推荐 -->
    <handle-service ref="refService" :prize="prize" @close="close"></handle-service>
    <!-- 福卡 -->
    <alipay-card ref="refAlipayCard" :prize="prize" :chance="chance" @continueRob="continueRob"></alipay-card>
    
    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',
         '0%',
        duration: 100
      })
    }
    

    3 最终效果

    图7

    5.参考

    1. animejs https://www.animejs.cn/
    2. Lottie https://airbnb.design/lottie/#get-started
  • 相关阅读:
    kernel reported iSCSI connection 1:0 error (1022-Invalid or unknown error code) state (3)
    [Visual Studio] pdb 和 exe 不match的情况
    What is the Makefile Target `.c.o` for?
    [Inno Setup] 区分Windows版本的一个例子
    CFLAGS [Makefile]
    Python 安装第三方插件时,报错 unable to find vcvarsall.bat
    【Inno Setup】Windows 版本号
    正则应用
    正则search与match的区别
    还是正则基础
  • 原文地址:https://www.cnblogs.com/tylerdonet/p/16009209.html
Copyright © 2020-2023  润新知