• 抽奖动画 - 鲤鱼跳龙门



    pageClass: home-page-class

    鲤鱼跳龙门动画

    1. 需求

    年中618营销活动要求做一个鲤鱼跳龙门的动画,产品参考了支付宝上的一个动画,要求模仿这个来做一个类似的动画。产品提供的截屏视频如下:

    图1
    从这个视频里得到的信息,我们可以把动画分解一下:

    • 321倒计时结束,动画开始播放。
    • 小河背景向下滚动,看上去小鱼在不停的向上游动,其实小鱼固定在屏幕中间位置。
    • 金币从屏幕顶部掉落,掉入小鱼的嘴里的时候金币消失,金币在掉落同时金币在旋转。
    • 用户点击“狂点”按钮,该按钮四周会出现一个光晕,并且变大变小。
    • 金币掉落完毕,出现龙门,小鱼跑到龙门上方。
    • 播放动画同时顶部有一个时钟倒计时,从6.18倒数到0。

    从视频上看,有一部分用css动画实现起来比较麻烦,例如,金币掉落完成之后,小鱼要转身,从背对观众变成面向观众,同时大小在变化,这些常见的css动画没法完全复原,初步判断这些是使用其他动画库来实现的,普通的css动画无法实现。
    我们事先要把这些告知产品,不然最后实现起来非常麻烦,因为本身活动项目开发时间非常短。

    2. 整体思路

    2.1 三二一倒计时

    三二一倒计时这个很简单,直接用文字显示的话不太美观,UI提供了三个4个张图片,我们可以按照数字分别命名3.png,2.png,1.png,0.png,然后使用setTimeInterval给变量做递减就可以了。倒计时结束后静态的小鱼变成一个游泳的小鱼,这里是一个gif图片,所以直接使用切换图片就可以了。

    2.2 河流

    小鱼向下游动,相对而言可以让小河向上滚动,在游戏背景上让河流绝对定位,设置position,初始bottom为0,播放动画,变为top为0,这样看上去是小鱼向上游动。

    2.3 金币坠落

    金币坠落也是使用绝对定位的方式,初始状态top是负值,隐藏在屏幕最上方,下落过程中逐渐变小,并且有旋转的动作,这里使用rotateY来控制旋转。待金币坠落到小鱼嘴的位置的时候,金币消失,模拟小鱼吃掉金币,这里设置大小为0,使用scale来缩放图片实现。

    2.4 “狂点”按钮

    用户点击狂点按钮时,小鱼的背后出现一个光晕,它由大变小,再由小变大,看上去小鱼是在加速,这个交互可以让动画更加生动。点击狂点按钮是,这个按钮自己本身也有一个由小变大,再由大变小的过程。

    2.5 跳龙门

    整个跳龙门的时间控制在6.18秒内,也就是河流滚动的时间也是6.18秒,结束后背景上面出现一个龙门图片,小鱼跳出屏幕。龙门图片最初设置opacity是0,跳出后是1,这样自然过度,如果使用显示&影藏来控制,看上去有点突兀。

    2.6 时钟

    最后顶部的倒计时时钟就很简单了,只要控制一个数字从6.18递减到0就满足需求了。

    3. 实现过程

    3.1 布局

    整个布局思路是绝对定位,整个背景fix定位在整个屏幕上,其他的元素使用absolute定位来固定位置。注意背景内的元素是absolute定位,都是居中显示,这里使用常用的方式left: 50%; margin-left: -(width/2);来设置左右居中。布局如下图1:

    图2 布局
    初始状态是这样,注意狂点按钮覆盖在小鱼上方,这个可以使用不同的z-index来实现,还有一些隐藏的元素,例如:金币图片,龙门图片,动画未开始的时候他么是隐藏的。

    html代码如下:

    <!-- 跃龙门游戏 -->
    <div class="dragon-gate-game" @touchmove.prevent.stop @mousewheel.prevent>
      <!-- 321倒计时 -->
      <mask-dialog ref="refCountdown">
        <div class="count-down">
          <img v-show="countDown == 3" class="coupon-btn" :src="require('../assets/images/animation/3.png')" alt="" />
          <img v-show="countDown == 2" class="coupon-btn" :src="require('../assets/images/animation/2.png')" alt="" />
          <img v-show="countDown == 1" class="coupon-btn" :src="require('../assets/images/animation/1.png')" alt="" />
          <img v-show="countDown == 0" class="coupon-btn" :src="require('../assets/images/animation/0.png')" alt="" />
        </div>
      </mask-dialog>
    
      <!-- 跳龙门 -->
      <div class="jump">
        <!-- 时钟倒计时 -->
        <div class="clock">{{ game.clock }}</div>
        <!-- 福字 -->
        <img v-for="(img, i) in game.blessing" :key="i" :src="img" class="blessing" alt="" />
        <!-- 小鱼 -->
        <div :class="[fish.name]" id="fish">
          <img :src="fish.src" alt="" class="img-fish"/>
          <img src="../assets/images/animation/bg-aureole.png" alt="" class="backdrop">
        </div>
        <!-- 狂点按钮 -->
        <img src="../assets/images/animation/btn1.png" :data-name="fish.name" alt="" class="btn-click" @click="jump" />
        <!-- 龙门 -->
        <img src="../assets/images/animation/bg-door.png" alt="" class="door" />
        <!-- 河 -->
        <img src="../assets/images/animation/bg-animation.jpg" alt="" class="river" />
      </div>
    </div>
    
    折叠

    给背景div设置禁止滚轮滚动,禁止拖放,防止它出现滚动条,配合fix定位,固定在屏幕上。其他的元素使用absolute定位,这里有两个兼容性问题要注意:

    • 注意元素定位使用bottom,不能使用top,防止部分浏览器底部工具栏遮挡"狂点"按钮,其他的元素也使用bottom。
    • 注意321倒计时不能使用js动态切换图片的路径,而是使用v-show判断,否则切换浏览器的时候在低端浏览器上会出现屏幕闪烁的现象,估计是造成页面重绘了。
      css代码如下:
    .dragon-gate-game {
      position: fixed;
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;
      z-index: 1;
      .loading,  .dragon-gate-game, .count-down, .jump {
        width: 100%;
        height: 100vh;
      }
      .count-down  {
        @include flex(center, center, row, nowrap);
        .coupon-btn {
          width: 400px;
        }
      }
      .jump {
        position: relative;
        overflow: hidden;
        .river, .clock, .water, .fish, .swim-fish, .btn-click, .door, .blessing {
          position: absolute;
          bottom: 0;
          left: 0;
          right: 0;
        }
        .clock {
          width: 239px;
          height: 64px;
          background: 34px center / 44px 44px no-repeat url("../assets/images/animation/icon-clock.png"), #000000;
          opacity: 0.4;
          border-radius: 32px;
          top: 120px;
          left: 50%;
          margin-left: -119px;
          z-index: 3;
          font-size: 36px;
          font-weight: 400;
          color: #FFFFFF;
          line-height: 64px;
          text-indent: 44px;
        }
        .river {
          z-index: 1;
          width: 750px;
          // height: 14039px;
        }
        .door {
          width: 750px;
          height: 960px;
          z-index: 3;
          opacity: 0;
        }
        .water {
          z-index: 2;
          width: 750px;
          height: 467px;
        }
        .fish, .swim-fish {
          position: relative;
          z-index: 3;
          left: 50%;
          img {
            position: absolute;
            width: 100%;
            left: 50%;
            margin-left: -50%;
          }
          .img-fish {
            z-index: 3;
          }
          .backdrop {
            position: absolute;
            z-index: 2;
            left: 50%;
            margin-left: -50%;
            opacity: 0;
          }
        }
        .fish {
          width: 259px;
          margin-left: -129px;
          top: 700px;
        }
        .swim-fish {
          width: 259px;
          margin-left: -129px;
          top: 600px;
        }
    
        .btn-click {
          z-index: 4;
          width: 240px;
          left: 50%;
          margin-left: -120px;
          bottom: 200px;
          // top: 1000px;
          // animation: .4s linear 1s infinite alternate btnZoom;
        }
        @keyframes btnZoom {
          from {
            transform: scale(0.8);
          }
          to {
            transform: scale(1.1);
          }
        }
    
        .blessing {
          width: 80px;
          margin-left: -40px;
          z-index: 3;
          left: 50%;
          top: -140px;
        }
      }
    }
    
    折叠

    3.2 倒计时

    data中定义变量countDown,初始值是3,使用setInterval来递减这个变量,这个逻辑相对来说比较简单,代码如下:

    //倒计时
    countDownClock() {
      this.$refs.refCountdown && this.$refs.refCountdown.show()
      this.timerInterval = null
      this.timerInterval = setInterval(() => {
        this.countDown--
        if (this.countDown < 0) {
          clearInterval(this.timerInterval)
          this.timerInterval = null
          this.$refs.refCountdown &&this.$refs.refCountdown.hidden()
          this.countDown = 3
          // 切换动画鱼
          // this.fish = this.game.swimFish
          // 播放动画
          // this.playAnime()
        }
      }, 1100)
    }
    
    折叠

    倒计时我们也放在一个透明蒙层里,最后两句切换动画鱼和静态鱼图片和播放小河,金币动画,暂时注释了,来看看效果:

    图3 倒计时

    3.3 播放动画

    开始播放动画时,首先把小鱼切换成那个gif图片,让小鱼动起来,这里在data数据中定义了一些数据。

    data(){
      return {
        pageShow: '',                       //页面显示
        percentage: '2%',                   //进度条变化
        countDown: 3,                       //321倒计时
        timerInterval: null,                //计时器,用于清除
        fish: {},                           //当前显示小鱼
        game: {
          finish: false,                    //是否已完成,回调后不能再点
          clock: 6.18,                      //时钟倒计时
          duration: 6180,                   //动画持续时间
          blessingOpacity: '1',             //显示金币
          fish: {name: 'fish', src: require('../assets/images/animation/bg-fish.png')},               //小鱼图片
          swimFish: {name: 'swim-fish', src: require('../assets/images/animation/fish-swim.gif')},    //游泳的小鱼
          blessing: Array(20).fill(require('../assets/images/losing-lottery/text-blessing.png')),      //金币
          clickCount: 0,                  //点击次数
        }
      }
    }
    
    折叠

    切换小鱼只需要上面注释的那句就可以了:this.fish = this.game.swimFish,然后执行下面的this.playAnime()来播放动画。
    这里还是使用anime.js动画库来播放,首先让小河向上滚动,同时让时钟从6.18倒数到0,同时让金币坠落,这三个动画前两个动画的时间是一致的,都是6.18秒,金币坠落的动画需要自己来估计,这里使用一个延迟,交错动画,延迟时间6.18*0.12,交错时间200毫秒,同时这个还和金币个数有关系,如果金币太少,动画后半部分没有金币坠落,金币太多6.18秒过了金币还没有落完,这都不是我们想要的结果,我们设置金币总共个数是20。
    6.18秒结束时要让龙门浮出,小鱼跳出龙门,龙门浮出通过设置opacity来实现,小鱼跳出,通过translateY实现,最后看代码如下:

    playAnime() {
      let tl = anime.timeline()
      //动画
      tl.add({
        //河流流动
        targets: '.river',
        easing: 'linear',
        duration: this.game.duration,
        top: 0,
        complete: () => {
          this.game.finish = true
          this.$emit('animeFinish', this.game.clickCount)
        }
      }).add({
        targets: this.game,
        clock: 0,
        easing: 'linear',
        round: 100,
        duration: this.game.duration,
      }, 0).add({
        //金币下落
        targets: '.blessing',
        easing: 'linear',
        delay: anime.stagger(200, {start: this.game.duration * 0.12}),
        keyframes: [
          {top: '30%', opacity: '1', scale: 0.8},
          {top: '45%', opacity: '0', scale: 0.5, rotateY: '360deg'}
        ],
      }, 0).add({
        //龙门浮出
        targets: '.door',
        easing: 'linear',
        delay: 200,
        opacity: 1
      }).add({
        //鱼跳出去
        targets: '#fish',
        // translateY: -100,
        translateY: -550,
        duration: 1000
      })
    }
    
    折叠

    结合data数据来看,前两个动画持续时间都是this.game.duration也就是6.18,金币坠落的动画需要我们调试,这里还使用了关键帧,动画进度是30%的时候,金币透明度是1,大小为原始大小的0.8倍,进度为45%的时候opacity是0,scale是0.5,沿Y轴旋转360度。金币坠落完成后龙门浮出,小鱼跳过龙门。这两个动画相对简单,一个是通过opacity来显示,一个通过translateY来隐藏。最后来看动画效果。

    图4 动画

    3.4 用户点击

    用户点击狂点按钮时有两个交互,一个是狂点按钮本身会有一个变大变小的过程,其次小鱼背后会出现一个光晕,这两个动画是每点击一次才播放一次的。每点击一次要纪录一下点击次数,这个调用抽奖接口的时候要用到,还有要判断动画是否已经结束,结束之后点击是没有什么效果的,当然这不是这里实现动画的关键。看下面的代码:

    jump() {
      let tl = anime.timeline()
      if (this.game.finish) return
      this.game.clickCount++
      console.log('this.game.clickCount')
      tl.add({
        targets: '.backdrop',
        duration: 1000,
        keyframes: [
          {opacity: 0.2},
          {opacity: 0.5},
          {opacity: 0.8},
          {opacity: 1.2},
          {opacity: 0.8},
          {opacity: 0.5},
          {opacity: 0.2},
          {opacity: 0},
        ]
      }).add({
        targets: '.btn-click',
        easing: 'linear',
        duration: 200,
        keyframes: [
          {scale: 0.9, opacity: 0.9},
          {scale: 0.8, opacity: 0.8},
          {scale: 0.7, opacity: 0.7},
          {scale: 0.6, opacity: 0.6},
          {scale: 0.8, opacity: 0.5},
          {scale: 0.9, opacity: 0.4},
          {scale: 1, opacity: 0.6},
          {scale: 1.1, opacity: 0.8},
          {scale: 1, opacity: 1}
        ]
      }, 0)
    }
    
    折叠

    小鱼图片和它背后的光晕都是使用绝对定位,但是小鱼的z-index要比光晕大,这样看起来光晕是在小鱼的下方。这两个动画都使用了关键帧来增强效果。点击效果图如下:

    图5 按钮点击

    最后就是调用接口,根据接口弹出中奖结果了,这和动画无关,只需要传一个参数,点击狂点按钮的次数。最后看一下整体效果,如下图6:

    图6 完整动画

    4.总结

    整个鲤鱼跳龙门动画已经介绍完,这个动画要考虑的元素很多,有小鱼,小鱼背后的光晕,龙门,金币,倒计时,小河等等,整个动画是由一个一个的小动画组合而成,只要把要考虑的细节考虑清楚,实现起来还是不难的。

    5.参考

    1. animate https://www.animejs.cn/
  • 相关阅读:
    Tomcat部署与优化
    读书记录 《就算生得暗淡,也要活得光彩》
    jenkins的nmp install命令无法下载包
    数据研发“新人”如何快速落地?
    20PIN直插百兆网络变压器 H82022D
    计算机网络-计算机网络体系结构-传输层
    论文阅读《LSD-SLAM: Large-Scale Direct Monocular SLAM》
    设置 wsl 桥接模式
    linux系统Jenkins工具介绍
    【华为OD机试真题 python】一种字符串压缩表示的解压 【2022 Q4 | 100分】
  • 原文地址:https://www.cnblogs.com/tylerdonet/p/16419370.html