Vue中使用create-keyframe-animation、動畫鉤子完成複雜動畫

Edon-發表於2019-04-08

Vue中使用create-keyframe-animation、動畫鉤子完成複雜動畫

如何實現這個動畫?

效果分析
點`start`的時候,我們把整個動畫拆分為兩種效果(過渡和動畫)。

1. 中間cd消失,下方播放條顯示,這是屬於`過渡` 
2. `過渡`開始的同時,cd同時移動、放大、縮小到左下方播放條  ,這屬於`動畫`

上面的效果是【過渡】加【動畫】同時使用完成的
複製程式碼
  • 對於第一種【過渡】,我們用vue中transition標籤,加設定v-enter、v-leave-to、v-enter-active、v-leave-enter即可完成
  • 對於第二種【動畫】,我們就要用keyframe來完成了。

這裡我們先完成第一種過渡
  • vue中模板節點
<template>
  <div class="index">
    <transition>
      <div class="cd-box" ref="cdWrapper" v-show="fullScreen">
    // CD圖片 (動畫的時候圖片初始位置)
        <img src="../assets/bj.png" alt="" class="bg">
      </div>
    </transition>
    <button @click="switchMode" style="position:absolute;top:0;left:10px;">start</button>
    <transition>
    // 下面播放狀態框
      <div class="mini-player-box" v-show="!fullScreen">
   // 狀態看裡面的圖片 (動畫的時候圖片結束位置)
        <div class="mini-img">
          <img src="../assets/bj.png" alt="" >
        </div>
      </div>
    </transition>
  </div>
</template>
複製程式碼

結構很簡單,基本就是兩個大div,然後把div的佈局按效果圖那些佈置。


css部分(省略佈區域性分)
.cd-box
    &.v-enter-active, &.v-leave-active
      transition: all 0.4s
    &.v-enter, &.v-leave-to
      opacity: 0
.mini-player-box
    &.v-enter-active, &.v-leave-active
      transition: all 0.4s 
    &.v-enter, &.v-leave-to
      transform: translate3d(0, 40px, 0)
      opacity: 0    
複製程式碼

這樣在fullScreen變數改變的時候,就會觸發【過渡】


這裡我們完成第二種動畫
  1. 首先安裝外掛npm i create-keyframe-animation 這個外掛是用js寫css的keyframe動畫用的,至於為什麼keyframe不在css裡面寫呢?那是因為螢幕大小不一樣,會導致需要移動的px不一樣,所以要動態計算。
  2. <transition>新增動畫鉤子
<transition 
      @enter="enter"
      @after-enter="afterEnter"
      @leave="leave"
      @after-leave="afterLeave"
    >  
      <div class="cd-box" ref="cdWrapper" v-show="fullScreen">
        <img src="../assets/bj.png" alt="" class="bg">
      </div>
    </transition>
複製程式碼
  1. 計算偏移量(中心點到中心的偏移,圖中紅線距離)
    Vue中使用create-keyframe-animation、動畫鉤子完成複雜動畫
// 獲得偏移量,以及scale
_getPosAndScale() {
   // 左下角圖片的寬度
     const targetWidth = 40
   // cd寬度
     const width = 300
     const scale = targetWidth / width
     // 這裡的 x,y要算,過程省略,無非就是加加減減,這的x,y都是算出來了的
     const x = -167.5
     const y = 497
     return {x ,y , scale}
   },
複製程式碼

x,y的數值代表什麼?見圖

Vue中使用create-keyframe-animation、動畫鉤子完成複雜動畫

這裡x為什麼是負的,y是正的呢?

因為瀏覽器的座標系的中心點是在左上角的,如圖

Vue中使用create-keyframe-animation、動畫鉤子完成複雜動畫

那麼動畫從cd中心到左下角,X偏移為負,y偏移為正

  1. 然後用animations外掛執行動畫鉤子
    // enter是指當 cd從隱藏到顯示的動畫,
    enter(el, done) {
      const {x, y, scale} = this._getPosAndScale()

      let animation = {
      // 第0幀的時候,先讓圖片縮小,顯示在右下角
        0: {
          transform: `translate3d(${x}px, ${y}px, 0) scale(${scale})`
        },
      // 60%的時候,讓圖片回到cd中心,變大
        60: {
          transform: `translate3d(0 ,0 , 0) scale(1.1)`
        },
     // 變回原來的尺寸,會有一個回彈的效果
        100: {
          transform: `translate3d(0 ,0 , 0) scale(1)`
        }
      }
      // 動畫的一些配置
      animations.registerAnimation({
        name: 'move',
        animation,
        presets: {
          duration: 400,
          easing: 'linear'
        }
      })
//執行動畫
    animations.runAnimation(this.$refs.cdWrapper, 'move', done)
    },
    afterEnter(){
    //執行完動畫之後,登出掉動畫
     animations.unregisterAnimation('move')
     this.$refs.cdWrapper.style.animation = ''
    },
    // leave是指 cd從顯示到隱藏的動畫
    leave(el, done) {
      this.$refs.cdWrapper.style.transition = 'all 0.4s'
      const {x, y, scale} = this._getPosAndScale()
      // 這裡我們只要直接移動變小就可以了
      this.$refs.cdWrapper.style['transform'] = `translate3d(${x}px,${y}px,0) scale(${scale})`
         
 // 監聽transitionend 事件在 CSS 完成過渡後觸發done回撥     
 this.$refs.cdWrapper.addEventListener('transitionend', () => {
        done()
      })
    },
    afterLeave() {
      this.$refs.cdWrapper.style.transition = ''
      this.$refs.cdWrapper.style['transform'] = ''
    }
複製程式碼

寫到這裡,我們就把剛開始的效果給寫完啦!

但在寫js的keyframe的時候

我們還可以加上rotate,讓動畫效果有一個回彈效果

let animation = {
        0: {
          transform: `translate3d(${x}px, ${y}px, 0) scale(${scale}) rotate(0deg)`
        },
        60: {
          transform: `translate3d(0 ,0 , 0) scale(1.1) rotate(365deg)`
        },
        100: {
          transform: `translate3d(0 ,0 , 0) scale(1) rotate(360deg)`
        }
      }

複製程式碼

所有原始碼

<template>
  <div class="index">
    <transition 
      @enter="enter"
      @after-enter="afterEnter"
      @leave="leave"
      @after-leave="afterLeave"
    >  
      <div class="cd-box" ref="cdWrapper" v-show="fullScreen">
        <img src="../assets/bj.png" alt="" class="bg">
      </div>
    </transition>
    <button @click="switchMode" style="position:absolute;top:0;left:10px;">start</button>
    <transition>
      <div class="mini-box" v-show="!fullScreen">
        <div class="mini-img">
          <img src="../assets/bj.png" alt="" >
        </div>
      </div>
    </transition>
  </div>
</template>

<script>
/* eslint-disable */
import animations from 'create-keyframe-animation'
export default {
  components: {},
  props: {},
  data() {
    return {
      fullScreen: true
    }
  },
  computed: {},
  watch: {},
  created() {},
  mounted() {
    // const {x, y, scale} = this._getPosAndScale()
    console.log(this._getPosAndScale())
    console.log(animations)
  },
  methods: {
    switchMode() {
      this.fullScreen = !this.fullScreen
    },
    _getPosAndScale() {
      const targetWidth = 40
      const paddingLeft = 20
      const paddingBottom = 20
      const paddingTop = 0
      const width = 300
      const scale = targetWidth / width
      const x = -(window.innerWidth / 2 - paddingLeft)
      const y = window.innerHeight - paddingTop - paddingBottom - width / 2
      return {x ,y , scale}
    },
    enter(el, done) {
      const {x, y, scale} = this._getPosAndScale()

      let animation = {
        0: {
          transform: `translate3d(${x}px, ${y}px, 0) scale(${scale}) rotate(0deg)`
        },
        60: {
          transform: `translate3d(0 ,0 , 0) scale(1.1) rotate(365deg)`
        },
        100: {
          transform: `translate3d(0 ,0 , 0) scale(1) rotate(360deg)`
        }
      }
      animations.registerAnimation({
        name: 'move',
        animation,
        presets: {
          duration: 400,
          easing: 'linear'
        }
      })
      animations.runAnimation(this.$refs.cdWrapper, 'move', done)
    },
    afterEnter(){
      animations.unregisterAnimation('move')
      this.$refs.cdWrapper.style.animation = ''
    },
    leave(el, done) {
      this.$refs.cdWrapper.style.transition = 'all 0.4s'
      const {x, y, scale} = this._getPosAndScale()
      this.$refs.cdWrapper.style['transform'] = `translate3d(${x}px,${y}px,0) scale(${scale})`
      // this.$refs.cdWrapper.style['transform'] = 'rotate(360deg)'
      // transitionend 事件在 CSS 完成過渡後觸發
      this.$refs.cdWrapper.addEventListener('transitionend', () => {
        done()
      })
    },
    afterLeave() {
      this.$refs.cdWrapper.style.transition = ''
      this.$refs.cdWrapper.style['transform'] = ''
    }
  }
}
</script>
<style lang="stylus" scoped>
.index
  background: #eee
  width: 100%
  height: 100%
  display : flex
  flex-direction: column
  justify-content : space-between
  align-items: center
  .cd-box
    display : flex
    justify-content : center
    align-items : center
    width: 300px
    height: 300px
    background: #eee
    border-radius: 50%
    &.v-enter-active, &.v-leave-active
      transition: all 0.4s
    &.v-enter, &.v-leave-to
      opacity: 0
    .bg
      width: 300px
      height: 300px
      border-radius: 50%
  .mini-box
    position: absolute
    bottom: 0
    right: 0
    left: 0
    display : flex 
    align-items center 
    border: 1px solid #555
    width: 100%
    height: 40px
    box-sizing : border-box
    &.v-enter-active, &.v-leave-active
      transition: all 0.4s 
    &.v-enter, &.v-leave-to
      transform: translate3d(0, 40px, 0)
      opacity: 0
    .mini-img
      height: 40px
      width: 40px
      box-sizing : border-box
      img
        height: 100%
        width: 100%
</style>
複製程式碼

相關文章