CSS3实现类似翻书效果的过渡动画

在VUE实战项目的中有一个加载推荐书籍的过渡动画,在项目中是使用JS实现。
CSS3实现类似翻书效果的过渡动画
当时看视频大概一个小时作用,拆分动画逻辑,写代码更是耗时。后来自己想着能不能用CSS动画直接写出来,折腾了半天,终于算是实现了。
可以查看加载动画地址

/*首先是DOM结构,不能像js一样分左右两边,正面翻为反面还要改变z-index,按照同样的布局也尝试过,没有用CSS动画写出来,逻辑太复杂。
这是一个类似翻书一样的动画效果,想着结构分为一页一页,一页旋转180°,完成时再改变“这一页”的z-index。每个page类的before伪类为正面,after伪类为反面;分解5页的在每一秒的动画,写出各自的animation。
*/
<div class="card">
    <div class="page"></div>
    <div class="page"></div>
    <div class="page"></div>
    <div class="page"></div>
    <div class="page"></div>
</div>



        .card{
            width: 50px;
            height: 50px;
            opacity: 0;
            position: relative;
            animation: .5s all;
        }
        .card .page{
            width: 50%;
            height: 100%;
            position: absolute;
            left: 50%;
            top: 0;
            transform-style: preserve-3d;
            transform-origin: left;
        }
        .card .page::before, .card .page::after{
            content: '';
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            border-radius: 0px 50px 50px 0px;
        }
        .card .page::after{
            border-radius: 50px 0px 0px 50px;
        }
        .card .page:nth-child(1){
            animation: animation1 2s linear infinite;
        }
        .card .page:nth-child(2){
            animation: animation2 2s linear infinite;
        }
        .card .page:nth-child(3){
            animation: animation3 2s linear infinite;
        }
        .card .page:nth-child(4){
            animation: animation4 2s linear infinite;
        }
        .card .page:nth-child(5){
            animation: animation5 2s linear infinite;
        }
        .card .page:nth-child(1)::before{
            background: aqua url("./images/star-right.png") center left/50% 50% no-repeat;
        }
        .card .page:nth-child(1)::after{
            background: hotpink url("./images/compass-left.png") center right/50% 50% no-repeat;
            transform: rotateY(180deg);
        }
        .card .page:nth-child(2)::before{
            background: hotpink url("./images/compass-right.png") center left/50% 50% no-repeat;
        }
        .card .page:nth-child(2)::after{
            background: coral url("./images/crown-left.png") center right/50% 50% no-repeat;
            transform: rotateY(180deg);
        }
        .card .page:nth-child(3)::before{
            background: coral url("./images/crown-right.png") center left/50% 50% no-repeat;
        }
        .card .page:nth-child(3)::after{
            background: cyan url("./images/gift-left.png") center right/50% 50% no-repeat;
            transform: rotateY(180deg);
        }
        .card .page:nth-child(4)::before{
            background: cyan url("./images/gift-right.png") center left/50% 50% no-repeat;
        }
        .card .page:nth-child(4)::after{
            background: yellowgreen url("./images/heart-left.png") center right/50% 50% no-repeat;
            transform: rotateY(180deg);
        }
        .card .page:nth-child(5)::before{
            background: yellowgreen url("./images/heart-right.png") center left/50% 50% no-repeat;
        }
        .card .page:nth-child(5)::after{
            background: aqua url("./images/star-left.png") center right/50% 50% no-repeat;
            transform: rotateY(180deg);
        }
@keyframes animation1 {
            0%{
                z-index: 1;
                transform: rotateY(180deg);
            }
            20%{
                z-index: 1;
                transform: rotateY(180deg);
            }
            40%{
                z-index: 3;
                transform: rotateY(360deg);
            }
            60%{
                z-index: 4;
                transform: rotateY(360deg);
            }
            60.0001%{
                z-index: 4;
                transform: rotateY(0deg);
            }
            80%{
                z-index: 5;
                transform: rotateY(0deg);
            }
            100%{
                z-index: 4;
                transform: rotateY(180deg);
            }
        }
        @keyframes animation2 {
            0%{
                z-index: 5;
                transform: rotateY(0deg);
            }
            20%{
                z-index: 4;
                transform: rotateY(180deg);
            }
            40%{
                z-index: 1;
                transform: rotateY(180deg);
            }
            60%{
                z-index: 3;
                transform: rotateY(360deg);
            }
            80%{
                z-index: 4;
                transform: rotateY(360deg);
            }
            100%{
                z-index: 5;
                transform: rotateY(360deg);
            }
        }
        @keyframes animation3 {
            0%{
                z-index: 4;
                transform: rotateY(0deg);
            }
            20%{
                z-index: 5;
                transform: rotateY(0deg);
            }
            40%{
                z-index: 4;
                transform: rotateY(180deg);
            }
            60%{
                z-index: 1;
                transform: rotateY(180deg);
            }
            80%{
                z-index: 3;
                transform: rotateY(360deg);
            }
            100%{
                z-index: 4;
                transform: rotateY(360deg);
            }
        }
        @keyframes animation4 {
            0%{
                z-index: 3;
                transform: rotateY(0deg);
            }
            20%{
                z-index: 4;
                transform: rotateY(0deg);
            }
            40%{
                z-index: 5;
                transform: rotateY(0deg);
            }
            60%{
                z-index: 4;
                transform: rotateY(180deg);
            }
            80%{
                z-index: 1;
                transform: rotateY(180deg);
            }
            100%{
                z-index: 3;
                transform: rotateY(360deg);
            }
        }
        @keyframes animation5 {
            0%{
                z-index: 2;
                transform: rotateY(0deg);
            }
            20%{
                z-index: 3;
                transform: rotateY(0deg);
            }
            40%{
                z-index: 4;
                transform: rotateY(0deg);
            }
            60%{
                z-index: 5;
                transform: rotateY(0deg);
            }
            80%{
                z-index: 4;
                transform: rotateY(180deg);
            }
            100%{
                z-index: 1;
                transform: rotateY(180deg);
            }
        }

再贴出JS实现:

<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>
data() {
  return {
    front: 0,
    back: 1,
  }
},
methods: {
    // flapCardList是存储着图片信息的对象数组,通过v-for循环和semiCircleStyle方法设置5组图片的背景。
    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) {
        //卡牌翻转,"front"选择右边卡片,否则选择左边卡片;然后改变dom元素的样式
        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() {
    //首先是翻转函数,每次翻转分正反两面。当动画至90°时,改变背面的z-index值,每一帧动画完毕执行rotate函数以改变样式。
    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()
    }
  },
  prepare() {
    const backFlapCard = this.flapCardList[this.back]
    backFlapCard.rotateDegree = 180
    backFlapCard._g = backFlapCard.g - 5 * 9
    this.rotate(this.back, 'back')
  },
  next() {
    const frontFlapCard = this.flapCardList[this.front]
    const backFlapCard = this.flapCardList[this.back]
    frontFlapCard.rotateDegree = 0
    backFlapCard.rotateDegree = 0
    frontFlapCard._g = frontFlapCard.g
    backFlapCard._g = backFlapCard.g
    this.rotate(this.front, 'front')
    this.rotate(this.back, 'back')
    this.front++
    this.back++
    const len = this.flapCardList.length
    if (this.front >= len) {
      this.front = 0
    }
    if (this.back >= len) {
      this.back = 0
    }
    // 动态设置zIndex
    // 100 -> 96
    // 99 -> 100
    // 98 -> 99
    // 97 -> 98
    // 96 -> 97
    // (0 - 1 + 5) % 5 = 4
    // (1 - 1 + 5) % 5 = 0
    this.flapCardList.forEach((item, index) => {
      item.zIndex = 100 - ((index - this.front + len) % len)
    })
    this.prepare()
  },
  startFlapCardAnimation() {
    this.prepare()
    this.task = setInterval(() => {
      this.flapCardRotate()
    }, this.intervalTime)
  },
  stopAnimation() {
    //动画停止时,清除所有setTimeout;否则再次进入动画出现错误。
    if (this.task) {
      clearInterval(this.task)
    }
    if (this.timeout) {
      clearTimeout(this.timeout)
    }
    if (this.timeout2) {
      clearTimeout(this.timeout2)
    }
    this.reset()
  },
  runAnimation() {
    //点击推荐,则动画开始(vuex中定义了flapCardVisible,watch监听其变化,为TRUE则动画开始)
    this.runFlapCardAnimation = true
    this.timeout = setTimeout(() => {
      this.startFlapCardAnimation()
      this.startPointAnimation()
    }, 300)
    this.timeout2 = setTimeout(() => {
      this.stopAnimation()
      //2500ms过渡动画结束,推荐图书显示
      this.runBookCardAnimation = true
    }, 2500)
  },
  watch: {
      flapCardVisible(v) {
        if (v) {
          this.runAnimation()
        }
      }
  },
}

写在最后:初次写CSS动画的总结,而且感觉自己的实现并不是很好,应该有更简单些的实现方法。从DOM结构设计,到动画过程分析还欠缺很多。并且一个动画如此费时,在实际项目中应该得不偿失。还是需要大量的练习,经常练手才可熟能生巧。