美文网首页前端开发那些事儿Vue前端
基于Vue的红包雨效果实现

基于Vue的红包雨效果实现

作者: ChasenGao | 来源:发表于2021-01-24 17:38 被阅读0次

    最近遇到了一个红包雨的需求,就大概这个样子:


    image.png

    具体效果可以进入拼多多查看。

    在实现这个效果之前,我先安利一个我们老大之前写的基于requestAnimationFrame实现的小动画框架 《chito》。https://redmed.github.io/chito/

    本文所述的需求均依赖此框架完成。

    它可以根据你传入的相关动画参数来为你创建流畅的补间动画。


    OK 回归正题。

    首先思考几个问题。

    1、红包要如何插入进来并展示掉落动画?
    思考结果:平常的方式就是创建一个以单个红包为纬度的组件,我需要创建多少个红包,进页面就用v-for来创建多少个dom。但是我觉得以我司运营的套路,这个红包以后肯定会各种复用,我总不能谁复用这个功能都copy一遍我的代码过去吧 那简直太灾难了。
    所以我准备将红包的插入方式改为函数调用的方式,就像在日常的后台管理系统中调用Element-UI中的 this.$message.success()一样,调用一次函数,红包就从上往下掉落一次,再根据不同的参数来单独控制每个红包的掉落速度、点击红包后对应的得分,甚至红包的颜色等等各项自定义属性。

    2、红包的掉落需要哪些参数?
    思考结果:以最最简单的交互来讲,可能只需要掉落速度,从何处掉落(红包掉落动画起始点的X轴),红包数量,红包雨持续的时间,点击红包后的反馈动画。
    现在我有两个组件,1、父组件来负责红包的展示,以及红包的掉落的属性(上述那些)。2、子组件就是红包组件。

    3、通过函数调用的组件如何编写和调用?
    这个貌似官方没有什么对应的文档,我也是从Element-UI的源码中抄过来的。

    4、未经允许直接复制本文的同学,我谢谢你。


    OK进入开发,首先就是红包组件

    首先我要定义一个红包容器,代码十分简单, 再给它一个简简单单的样式

    <template>
      <div class="box" ref="packet" @touchstart.once="handleClick" v-show="hidden">
      </div>
    </template>
    <style scoped lang="stylus">
    * {
      user-select: none
      outline none
    }
    
    .box {
      width 0.592rem
      height 0.86rem
      position absolute
      top -1rem
      background url("https://coolcdn.igetcool.com/p/2021/1/f611d435ffb8c8c43c6983d807e27a65.png?_296x430.png")
      background-repeat no-repeat
      background-size contain
      transform rotate(0deg)
    }
    </style>
    

    这样,屏幕上就会出现一个红包,图是我随便找的一个。


    image.png

    红包应该是竖着的,我随便截了个图而已。

    红包组件的详细代码,每一行都有注释,我不相信你看不懂😄

    <template>
      <div class="box" ref="packet" @touchstart.once="handleClick" v-show="hidden">
      </div>
    </template>
    
    <script>
    const {Animation, Clip} = require('chito')
    export default {
      name: "bonusRain",
      data() {
        return {
          // 代表红包该显示还是不显示
          hidden: true,
          // 用于存放chito生成的动画实例
          animation: null,
          // 用于存放从父组件传进来的配置项
          options: null,
          // 用于记录红包是不是已经掉落
          isDropped: false,
          // 用于判断红包在掉落的时候是顺时针旋转还是逆时针旋转
          rotateComputed: 1
        }
      },
      methods: {
        // 点击事件
        handleClick(e) {
          this.$nextTick(() => {
            // 因为每个红包都只能点击一次,所以点击红包后,就让动画停止
            this.animation.stop()
            // 因为红包点击后也代表了销毁,所以在这里也要调用dropped事件
            if (this.isDropped === false) {
              this.options.onDropped()
              this.isDropped = true
            }
    
            // 自定义的click事件,让事件能够分发出去,因为不是直接通过dom的方式向
            // 父组件插入的组件,所以不能用$.emit分发事件
            if (this.hidden === true) {
              this.options.onClick(e)
            }
    
            // 点击红包后修改红包样式
            this.$refs['packet'].style.transform = 'rotate(0deg)'
            this.$refs['packet'].style.background = `url(https://coolcdn.igetcool.com/p/2021/1/c1a3568325f4fa97f843850aaa9713a7.jpg?_500x511.jpg)`
            this.$refs['packet'].style.backgroundSize = 'contain'
            this.$refs['packet'].style.backgroundRepeat = 'no-repeat'
            this.$refs['packet'].style.animation = 'unset'
    
            // 修改完样式总不能直接消失,所以适当给一个延时
            setTimeout(() => {
              this.hidden = false
            }, 500)
          })
    
    
        },
        show(obj) {
          // 把传进来的配置项赋值给options
          this.options = obj
    
          // 自定义了一个beforShow的钩子,这样动画在初始化的时候想执行什么方法也方便
          if(this.options.beforeShow){
            this.options.beforeShow()
          }
    
          // 如果穿进来了一个封面图片,就替换掉当前的红包封面
          if(obj.cover){
            this.$refs['packet'].style.background = `url(${obj.cover})`
            this.$refs['packet'].style.backgroundSize = 'contain'
            this.$refs['packet'].style.backgroundRepeat = 'no-repeat'
          }
    
          // 创建一个动画剪辑
          let clip = new Clip({
            // 剪辑持续的时间,如果掉落距离是固定的,那么掉落时间就决定了红包掉落的速度
            duration: obj.speed || 2000,
            // 剪辑重复一次,因为每个红包都是独立的
            repeat: 1
          }, {
            // 掉落的路线,从 -100开始到屏幕高度
            y: [-100, document.documentElement.clientHeight]
          })
    
          // 红包在掉落的过程中,每一次运动都会触发clip的update事件,
          // 如果你需要一些花里胡哨的效果,可以在这里定义
          clip.on('update', (ev) => {
            var keyframe = ev.keyframe;
            // 因为要操作dom 所以需要用nextTick
            this.$nextTick(() => {
              // 掉落的过程中动态改变红包的y轴位置
              this.$refs['packet'].style.top = keyframe.y + 'px';
              // 根据配置项的x轴位置来设置红包的x轴位置
              this.$refs['packet'].style.left = obj.xAxis + 'px'
              // 红包掉落的时候让它旋转起来
              this.$refs['packet'].style.transform = `rotate(` + (ev.progress * 180 * this.rotateComputed) +`deg)`
            })
          });
          // 创建Animation实例
          this.animation = new Animation();
          // 把创建的剪辑添加到Animation实例中
          this.animation.addClip([clip]);
        },
        start(){
          // 还在奇怪为什么我rotateComputed给了个数字1么?
          // 这里就告诉你,如果是1它就顺时针旋转,-1就是逆时针旋转
          this.rotateComputed = (Math.random() * 10) > 5 ? 1:-1
    
          // 让动画开始播放
          this.animation.start()
    
          // 动画完成播放的事件
          this.animation.on('complete', () => {
    
            // 动画结束之后回调onDropped事件
            if (this.isDropped === false) {
              this.options.onDropped()
    
              // 动画完成后,把isDropped的值修改为是
              // 代表红包已经掉落,理论上没什么用,但是玩意需要父组件做判断呢?
              this.isDropped = true
            }
            // 动画播放完成后,隐藏红包
            this.hidden = false
          })
        }
      }
    }
    </script>
    
    <style scoped lang="stylus">
    * {
      user-select: none
      outline none
    }
    
    .box {
      width 0.592rem
      height 0.86rem
      position absolute
      top -1rem
      background url("https://coolcdn.igetcool.com/p/2021/1/f611d435ffb8c8c43c6983d807e27a65.png?_296x430.png")
      background-repeat no-repeat
      background-size contain
      transform rotate(0deg)
    }
    
    </style>
    
    

    因为要通过函数调用的方式调用组件,所以还需要写一个插件。

    源码如下:

    // 引入Vue
    import Vue from 'vue'
    // 引入红包组件
    import bonusItem from './bonusItem.vue';
    
    // 红包实例
    let packet;
    
    // 组件挂载
    function createItem(args) {
    
      // 用vue渲染红包组件并挂载
      const vnode = new Vue({
        render: h => h(bonusItem)
      }).$mount()
    
      // 将组件添加到body上
      document.body.appendChild(vnode.$el)
    
      // 返回当前组件的实例
      return vnode.$children[0]
    }
    
    export function showPacket(args) {
      // 创建组件
      packet = createItem(args)
    
      // 将组件实例暴露出去
      return packet
    }
    export default showPacket
    
    

    这样,我们就可以通过调用函数的方式动态插入组件了

    OK,接下来就是父组件的调用,完整代码如下:

    <template>
      <div class="container">
        分数{{point}}
        <br>
        <p v-if="game">游戏结束</p>
        <br>
        <button @click="start">开始游戏</button>
      </div>
    </template>
    
    <script>
    // 引入红包组件
    import showPacket from './bonusItem.js'
    export default {
      name: "test",
      data(){
        return {
          // 倒计时
          time: 10,
          // 红包数量
          itemCount: 1,
          // 红包实例存储栈
          dropStack: [],
          // 分数记录
          point: 0,
          // 已经销毁的红包数量
          dropped: 0,
          // 游戏状态
          game:false,
        }
      },
      // 进页面的时候,让页面高度固定为100vh并且不能滚动
      beforeCreate() {
        document.body.style.maxHeight = '100vh'
        document.body.style.overflow = 'hidden'
      },
      // 离开页面之前 恢复,避免不影响其它页面
      beforeDestroy() {
        document.body.style.maxHeight = 'unset'
        document.body.style.overflow = 'unset'
      },
      mounted() {
        // 创建红包DOM,并把每次函数执行返回的dom实例放到存储栈中
        let arr = []
        for(let i in this.itemStack){
          let instance = showPacket()
          arr.push(instance)
          instance.show({
            // 遍历x轴,因为x轴是随机生成的,所以红包掉落的起始位置也是随机的
            xAxis:  this.itemStack[i],
            // 红包掉落的速度
            speed:  3000,
            // 红包点击事件
            onClick: () =>{
              // 每次点击,分数+1
              this.point++
            },
            // 红包销毁事件
            onDropped: () => {
              // 销毁数量加1
              ++this.dropped
              // 如果销毁数量等于红包数量,那么游戏停止
              if(this.dropped === this.itemCount){
                this.game = true
              }
            }
          })
        }
        // 将创建的红包实例存入栈中
        this.dropStack = arr
    
      },
      methods: {
        // 红包雨开始
        start(){
          this.game = false;
          // 遍历红包栈,并根据事件平均分配掉落的时机
          for(let i in this.dropStack){
            setTimeout(() => {
              this.dropStack[i].start()
    
              // 假设红包雨持续的事件是10秒,红包数量为20个,那么每个红包掉落的时机就是
              // 当前遍历的索引 * (10秒 * 1000毫秒 / 红包数量)
              // 等于每 i * 10000毫秒 / 20 掉落一个。
              // 这里不懂的可以问我
            }, i * (this.time * 1000 / this.itemCount))
          }
        },
      },
      computed: {
        // 按照红包数量生成对应的X轴随机数
        itemStack(){
          let arr = []
          for(let i = 0; i < this.itemCount; i++){
            // 保证红包的x轴在固定的范围内,根据实际需求控制
            arr.push(Math.floor(Math.random() * ( 300 - 20) + 20))
          }
          return arr
        }
      }
    }
    
    </script>
    
    <style scoped lang="stylus">
    .container {
      background-size contain
      background-repeat no-repeat
      height 100vh
      overflow hidden
      color #000
      font-size 0.2rem
    }
    </style>
    
    

    最终的效果如下:


    image.png

    因为没有工具录制gif 所以截了个示例图,快去自己试试吧。

    搬运本文,请注明原文地址,谢谢。

    相关文章

      网友评论

        本文标题:基于Vue的红包雨效果实现

        本文链接:https://www.haomeiwen.com/subject/bcmmzktx.html