推荐卡片翻转动画和烟花动画的实现

522 阅读3分钟

以下为实际效果:

具体的实现过程如下: 因为我这个是给一个图书应用做的,所以给很多标签起名字都是book开头的,在这里先解释一下。 看这个动画我们先分析拆解一下:

1.弹出卡片

2.卡片翻转动画(难点)

3.烟花动画(难点)

4.弹出推荐图书

其中第一次和第四次的弹出动画是使用的CSS3的keyframes属性做出来的。

动画的弹出是一个逐渐变大并且又快到慢的过程,我们称之为缓入动画开头慢结尾快。

他的实现使用了animation里面的ease-in属性动画的最终状态保持使用了both属性,both属性很有意思,它集成了forwards即被应用目标会保留执行过程中的属性,backwards的目标在执行时立即应用被定义的值。并在animation-delay期间保留。

下面的css代码大部分集成了一些基本的布局设置,可以专注于&.animation{}部分的代码

我给1和4弹出卡片应用的动画:

    .book-card{
      position: relative;
      width: 65%;
      box-sizing: border-box;
      border-radius: px2rem(15);
      background: white;
      /*animation-fill-mode:
动画的最后(达到100%)时的状态,
取值有:backward(回到初始状态)、forwards(停在最终状态)、none、both。
动画将遵循forwards和backwards的规则,从而在两个方向上扩展动画属性。*/
      &.animation {
        animation: scale .3s ease-in both;
        @keyframes scale {
          0% {
            transform: scale(0);
            opacity: 0;
          }
          100% {
            transform: scale(1);
            opacity: 1;          }
        }
      }
      .book-card-wrapper{
        width: 100%;
        height: 100%;
        margin-bottom: px2rem(30);
        @include columnTop;
        .img-wrapper{
          width: 100%;
          margin-top: px2rem(20);
          @include center;
          .img {
            width: px2rem(90);
            height: px2rem(130);
          }
        }
        .content-wrapper{
          padding: 0 px2rem(20);
          margin-top: px2rem(20);
         .content-title{
           color: #333;
           font-weight: bold;
           font-size: px2rem(18);
           line-heigth:px2rem(20);
           max-height: px2rem(40);
           text-align: center;
           @include ellipsis2(2)
         }
          .content-author{
            margin-top: px2rem(10);
            text-align: center;
          }

          .content-category{
            color: #999;
            font-size: px2rem(14);
            margin-top: px2rem(10);
            text-align: center;
          }
        }
        .read-btn{
          position: absolute;
          bottom: 0;
          left: 0;
          z-index: 1100;
          width: 100%;
          border-radius: 0 0 px2rem(15) px2rem(15);
          padding: px2rem(15) 0;
          text-align: center;
          color: white;
          font-size: px2rem(14);
          background: $color-blue;
        }
      }
    }

我给2卡片翻转动画弹出使用的动画:

    .flap-card-bg {
      position: relative;
      width: px2rem(64);
      height: px2rem(64);
      border-radius: px2rem(5);
      background: white;
      transform: scale(0);
      opacity: 0;
      &.animation{
        animation: flap-card-move .3s ease-in both;
      }
      @keyframes flap-card-move {
        0% {
          transform: scale(0);
          opacity: 0;
        }
        50% {
          transform: scale(1.2);
          opacity: 1;
        }
        75% {
          transform: scale(.9);
          opacity: 1;
        }
        100% {
          transform: scale(1);
          opacity: 1;
        }
      }
      .flap-card{
        width: px2rem(48);
        height: px2rem(48);
        @include absCenter;
        .flap-card-circle{
          display: flex;
          width: 100%;
          height: 100%;
          .flap-card-semi-circle{
            flex: 0 0 50%;
            width: 50%;
            height: 100%;
            background-repeat: no-repeat;
            backface-visibility: hidden;
          }
          .flap-card-semi-circle-left{
            border-radius: px2rem(24) 0 0 px2rem(24);
            background-position: center right;
            transform-origin: right;
          }
          .flap-card-semi-circle-right{
            border-radius: 0 px2rem(24) px2rem(24) 0;
            background-position: center left;
            transform-origin: left;
          }
        }
      }
      .point-wrapper{
        z-index: 1500;
        @include absCenter;
        .point{
          border-radius: 50%;
          @include absCenter;
          &.animation {
            @for $i from 1 to length($moves) {
              &:nth-child(#{$i}) {
                @include move($i);
              }
            }
          }
        }
      }

    }

我给3烟花弹出使用的动画:

      .point-wrapper{
        z-index: 1500;
        @include absCenter;
        .point{
          border-radius: 50%;
          @include absCenter;
          &.animation {
            @for $i from 1 to length($moves) {
              &:nth-child(#{$i}) {
                @include move($i);
              }
            }
          }
        }
      }

可以看到那些小卡片的翻转,这个实现的背后是非常复杂的,我将五张卡片,让它们按照一定的逻辑进行顺时针翻转,颜色的由浅入深,还有一个接一个。这个到底怎么实现呢?请看下文:

这里是标签代码,绑定了伪类和ref,可以使用$refs对象来操作dom

  <div class="flap-card" v-for="(item, index) in flapCardList" :key="index"
       :style="{zIndex: item.zIndex}">
         <div class="flap-card-circle">
           <div class="flap-card-semi-circle flap-card-semi-circle-left" :style="semiCircleStyle(item, 'left')" ref="left"></div>
           <div class="flap-card-semi-circle flap-card-semi-circle-right" :style="semiCircleStyle(item, 'right')" ref="right"></div>
         </div>
       </div>

可以看到我是用了v-for循环展示flapCardList中的图片:

export const flapCardList = [
  {
    r: 255,
    g: 102,
    _g: 102,
    b: 159,
    imgLeft: 'url(' + require('@/assets/images/gift-left.png') + ')',
    imgRight: 'url(' + require('@/assets/images/gift-right.png') + ')',
    backgroundSize: '50% 50%',
    zIndex: 100,
    rotateDegree: 0
  },
  {
    r: 74,
    g: 171,
    _g: 171,
    b: 255,
    imgLeft: 'url(' + require('@/assets/images/compass-left.png') + ')',
    imgRight: 'url(' + require('@/assets/images/compass-right.png') + ')',
    backgroundSize: '50% 50%',
    zIndex: 99,
    rotateDegree: 0
  },
  {
    r: 255,
    g: 198,
    _g: 198,
    b: 102,
    imgLeft: 'url(' + require('@/assets/images/star-left.png') + ')',
    imgRight: 'url(' + require('@/assets/images/star-right.png') + ')',
    backgroundSize: '50% 50%',
    zIndex: 98,
    rotateDegree: 0
  },
  {
    r: 255,
    g: 102,
    _g: 102,
    b: 159,
    imgLeft: 'url(' + require('@/assets/images/heart-left.png') + ')',
    imgRight: 'url(' + require('@/assets/images/heart-right.png') + ')',
    backgroundSize: '50% 50%',
    zIndex: 97,
    rotateDegree: 0
  },
  {
    r: 59,
    g: 201,
    _g: 201,
    b: 22,
    imgLeft: 'url(' + require('@/assets/images/crown-left.png') + ')',
    imgRight: 'url(' + require('@/assets/images/crown-right.png') + ')',
    backgroundSize: '50% 50%',
    zIndex: 96,
    rotateDegree: 0
  }
]

每一张图片都是用左右两部分拼接起来的。

可以关注到使用伪类给图片绑定上它的zIndex,出场顺序

 :style="{zIndex: item.zIndex}"

基本布局代码如下:

      .flap-card{
        width: px2rem(48);
        height: px2rem(48);
        @include absCenter;
        .flap-card-circle{
          display: flex;
          width: 100%;
          height: 100%;
          .flap-card-semi-circle{
            flex: 0 0 50%;
            width: 50%;
            height: 100%;
            background-repeat: no-repeat;
            backface-visibility: hidden;
          }
          .flap-card-semi-circle-left{
            border-radius: px2rem(24) 0 0 px2rem(24);
            background-position: center right;
            transform-origin: right;
          }
          .flap-card-semi-circle-right{
            border-radius: 0 px2rem(24) px2rem(24) 0;
            background-position: center left;
            transform-origin: left;
          }
        }
      }

一些methods对图像进行操作:

   // 前面先展示图片在动的时候,后面的图片也需要做准备工作就是 也要动起来
      prepare() {
        const backFlapCard = this.flapCardList[this.back]
        backFlapCard.rotateDegree = 180
        backFlapCard._g = backFlapCard - 5 * 9
        this.rotate(this.back, 'back')
      },

颜色变化

      semiCircleStyle(item, dir) {
        return {
          backgroundColor: `rgb(${item.r},${item.g},${item.b})`,
          backgroundSize: item.backgroundSize,
          backgroundImage: dir === 'left' ? item.imgLeft : item.imgRight
        }
      },

角度变化

      rotate(index, type) {
        const item = this.flapCardList[index]
        let dom
        if (type === 'front') {
          dom = this.$refs.right[index]
        } else {
          dom = this.$refs.left[index]
        }
        dom.style.transform = `rotateY(${item.rotateDegree}deg)`
        dom.style.backgroundColor = `rgb(${item.r},${item._g},${item.b})`
      },

卡片转动起来以后属性的变化

      flapCardRotate() {
        const frontFlapCard = this.flapCardList[this.front]
        const backFlapCard = this.flapCardList[this.back]
        frontFlapCard.rotateDegree += 10
        frontFlapCard._g -= 5
        backFlapCard.rotateDegree -= 10
        if (backFlapCard.rotateDegree < 90) {
          backFlapCard._g += 5
        }
        if (frontFlapCard.rotateDegree === 90 && backFlapCard.rotateDegree === 90) {
          backFlapCard.zIndex += 2
        }
        this.rotate(this.front, 'front')
        this.rotate(this.back, 'back')
        if (frontFlapCard.rotateDegree === 180 && backFlapCard.rotateDegree === 0) {
          this.next()
        }
      },

卡片运行起来的逻辑

      startFlapCardAnimation() {
        this.prepare()
        this.task = setInterval(() => {
         this.flapCardRotate()
        }, this.intervalTime)
      },