【threejs + Vue】寫一個不怎麼酷炫的3D圖片輪播組件 -【附組件源碼 + 註釋】

前言

寫這個組件的主要動機是想整點活(絕對不是由於本身鴿了兩年沒寫文章(迫真)(笑)),順便把本身這段時間來積累的一些知識用一下,加深一下印象,否則就快要忘光了(雖然早晚得忘(⌐■_■)–︻╦╤─)。vue

而後寫的是一個Vue組件,你問爲何不是像之前那樣寫原生js插件?一個字:懶(義正詞嚴),固然最直接的緣由是我一開始寫的時候就是用的vue,因此後面就懶得改了(( ̄ー ̄)),若是不是用的vue的童鞋能夠本身去還原,把this什麼的弄一下,接口位置對上應該就能夠直接用了。 web

由於組件內用了threejs和tweenjs,記得npm installchrome

npm install --save three @tweenjs/tween.js

廢話就講到這裏了,下面介紹一下組件內主要的一些參數,具體細節和邏輯不會講,由於很煩,因此大家本身去看代碼吧。大部分地方註釋都打了,對threejs沒研究的同窗就不要去深究相應部分的代碼,由於很難看懂npm

演示

codepen
寇得噴打不開的能夠看下面的地址
jsruncanvas

代碼介紹

邏輯代碼主要分爲兩部分,一部分是在外部定義的一些局部變量和方法(這裏主要是用於保存一些我不但願你能直接碰到的變量和方法等等,如:scene,還有就是在vue data下面掛載屬性過多的對象容易出現性能問題) 第二部分就是在vue內部定義的一些屬性和方法(以_開頭的方法也是我不但願你直接調用的東西,這裏大部分都是中間邏輯的處理方法,因此你若是沒看懂就儘可能不要動)瀏覽器

下面介紹一些控制相關的屬性

props下面的屬性不講,基本上都能看懂,而後每一個圖片對應的item數據結構格式以下:數據結構

{
  src: '', // 連接,必填
  size: [], // 選填, 當前圖片對應的在X軸和Y軸上的立方體的數目,默認是[16, 9](data屬性的defaultSize),也是大部分圖片的寬高比,固然這兩個值並非必定要寫到沒有約數,好比[32, 18],或者[24, 10]都是能夠的,但我不建議數目寫太大,主要是浪費性能,還有就是沒有在一個size時候過渡好看
  animate: {
    name: '', // 圖片過渡動畫選擇的動畫種類,目前我只寫了三種translate(平移)/cover(覆蓋)/fade(變淡)
    type: '', // 在上面name肯定動畫種類之後,在其內部選擇相應的動畫小類, 如name是fade,type能夠選擇In,Out,InOut和OutIn, 具體參數能夠在_animateImage方法下面查看
    duration: 3000, // 持續時間,
    delay: 0, // 延時
    easing: TWEEN.Easing.Quadratic.In // tweenjs 內置的動畫曲線類型
  }
}

data下面的屬性:app

// 將canvas的精度放大多少倍, 主要是爲了保證圖片不失真,以寬爲200,高爲100的canvas舉例,直接效果就等同於
// <canvas width="600" height="300" style="width: 200px; height: 100px;">
canvasScale: 3,
// webgl canvas, 用於保存繪製3D效果的canvas element
canvas: null,
// 繪圖canvas, 當前圖片對應離屏canvas
imageCanvas: null,
// 容器
wrapper: null,
// 是否當前資源正在加載中,用於控制行爲進行
cvLoading: true,
// 當前是否正在動畫中, 防止頻繁切換
isAnimating: false,
// webGl canvas尺寸
cWidth: 0,
cHeight: 0,
// 當前圖片尺寸
iWidth: 0,
iHeight: 0,
// 當前場景的橫縱比,用於計算,會根據當前圖片的狀況變化,不用動
size: [16, 9],
// 默認橫縱比,這裏也是用來動態計算canvas高度的東西,使畫布寬高比爲16/9,在mounted裏面有用到,
// 再就是在傳入的item數據裏若是沒有設置size的時候,也被拿來當成默認值,此值是能夠修改的,能夠實際根據須要數值進
// 行修改,好比個人項目裏大部分的圖片都是正方形的,那我能夠把defaultSize設置成[10, 10]
defaultSize: [16, 9],
// 當前圖片序號,自動計算,不用手動修改。若是你想從第1張圖片直接跳到第3張,調用changeImage方法就好了,這裏會自動變化
currentIndex: 0,
// 動態計算的容器高度
acHeight: 0,
// 偏移延遲,主要用於進行動畫過渡時,因爲動畫效果須要耗費的額外延時時間將保存到這裏,存最大值(計算用,不用管)
offsetDelay: 0,
// 下面的屬性基本上都是用來控制立方體各個階段的動畫效果和時間的,能夠手動設置
// 注意:這裏不控制圖片的動畫,僅僅是立方體的動畫而已,動畫流程爲: 
// 隱藏hidden(若是本張圖片的size比上一張圖片的size要小,不須要用到那麼多立方體,則多餘的立方體會先進行過渡隱藏)-> 變化change -> 開始定位start -> 完成定位end
// 內置的物體運動相關的控制參數
hiddenOption: {
  duration: 2000,
  delay: 0,
  easing () {
    return TWEEN.Easing.Quadratic.InOut
  }
},
changeOption: {
  duration: 2000,
  delay: 0,
  easing () {
    return TWEEN.Easing.Quadratic.Out
  }
},
startOption: {
  duration: 1000,
  delay: 0,
  easing () {
    return TWEEN.Easing.Quadratic.Out
  }
},
endOption: {
  duration: 500,
  delay: 0,
  easing () {
    return TWEEN.Easing.Quadratic.In
  }
},
// 一次動做完成後通過多少ms進行下一次動做
spaceTime: 3000,
// 立方體之間的間隔, 因爲3D視角的緣由,實際上cube之間的距離不必定是設定值那麼寬
cubeSpace: 0,
// 當進行圖片切換時,每一個立方體進行移動的邊界範圍(在changeTween方法裏有用到)
cubeChangeRange: {
  useAnimation: true, // 是否進行中間的過渡動畫
  delay: 100, // 隨機延遲時間範圍
  x: 800, // 偏移值基準,下同 (實際使用時不必定會所有用到)
  y: 400,
  z: 100
}

methods下面的方法:dom

initCanvas:初始化3d場景中的一些必要元素
updateSize: 更新各部分尺寸,主要在容器尺寸變化後須要進行自適應時調用
changeImage(idx): 切換圖片,傳入對應item的下標
update: 動畫循環,不用管
toNext: 下一張圖片
toPrev: 上一張圖片
play: 進行自動播放時會被調用,能夠經過修改props傳入的auto值進行自動播放
_getChangeTweenMoveData: 獲取一個change過渡的運動變化參數,隨機返回一組效果,具體動畫邏輯在局部變量的moveData裏面
_changeUVs: 更新各立方體的uv貼圖
_initCube: 處理立方體的生成和複用以及初始化
_animateCube: 複用立方體的時候,在用於圖片size變化致使立方體大小須要從新計算的時候提供一個平滑的動畫過渡
_initPosition: 按照行爲流程開始計算並運動全部立方體
_hiddenTween: 多餘立方體的隱藏動畫處理
_startTween: 基本肯定各立方體應在位置後進行的動畫處理
_endTween: 最後完成各立方體位置移動的動畫
_changeTween: 開始進行圖片切換時按照獲得的運動參數打亂全部立方體位置的過渡動畫
_initImage: 開始進行圖片切換時的一些處理
_switchImage: 圖片開始切換
_animateImage: 圖片切換的動畫處理,內部包含了全部的切換效果,在item數據的name和type屬性要用到的參數在此方法內部可找到
_loadImages,_loadImage: 圖片加載處理

瀏覽器要求

chrome內核要69以上,否則會產生不支持離屏canvas這樣的bug,紋理不會被渲染
IE爬ide

組件源碼(尻♂過去就能用(手動滑稽)):

<template>
  <section class="slider-3d c--slider-3d">
    <!--注意這裏的v-resize是監聽窗口變化的指令,大家沒有的得本身寫一下-->
    <div class="slider-3d-box"
         ref="wrapper"
         :style="{height: acHeight + 'px'}"
         v-resize="updateSize"
    >
      <canvas id="slider-3d-cv" ref="canvas"></canvas>
    </div>
  </section>
</template>

<script>
  import * as THREE from 'three'
  const TWEEN = require('@tweenjs/tween.js').default
  // 定義場景,渲染器,相機變量
  let [scene, renderer, camera] = Array.of(null, null, null)
  // 定義材質,紋理變量
  let [material, texture] = Array.of(null, null)
  // 定義存放立方體的隊列
  let meshs = []
  // 定義相機在初始位置肯定後在Z軸上的偏移量(爲了給視角留空間,值越小,視野範圍內的物體看起來越大)
  const offsetZ = 25
  // 定義圖片發生切換動做時存放上一張圖片的變量
  let prevImage = null
  // 存放生成的離屏canvas
  let drawImages = []
  // 拉遠鏡頭
  function farCamera () {
    // 保存初始數據
    let {x, y, z} = camera.position
    let pos = {x, y, z}
    camera.userData.pos = {...pos}
    new TWEEN.Tween(pos)
      .to({...pos, z: pos.z * 1.4}, 1500)
      .easing(TWEEN.Easing.Quadratic.Out)
      .onUpdate(() => {
        camera.position.set(pos.x, pos.y, pos.z)
      })
      .start()
  }
  // 還原鏡頭
  function restoreCamera () {
    let {x, y, z} = camera.position
    let pos = {x, y, z}
    let target = camera.userData.pos
    if (!target) return false
    if (pos.x === target.x && pos.y === target.y && pos.z === target.z) return false
    new TWEEN.Tween(pos)
      .to({...target}, 1500)
      .easing(TWEEN.Easing.Quadratic.Out)
      .onUpdate(() => {
        camera.position.set(pos.x, pos.y, pos.z)
      })
      .start()
  }
  // 存放當前圖片切換時的過渡動畫類型
  const moveType = {
    // 隨機
    random (range) {
      return {
        x: -range.x / 2 + Math.random() * range.x,
        y: -range.y / 2 + Math.random() * range.y,
        z: -range.z / 2 + Math.random() * range.z,
        delay: Math.random () * range.delay >> 0
      }
    },
    // 排序延時,左下
    orderDelayLB (range, i, j, size) {
      farCamera()
      let [ox, oy] = size
      return {
        x: -10 - (ox - i) * 5,
        y: -15 - (oy - j) * 5,
        z: range.z / 10,
        delay: Math.min(range.delay, 100) / 2 * (i + j)
      }
    },
    // 排序延時,左上
    orderDelayLT (range, i, j, size) {
      farCamera()
      let [ox, oy] = size
      return {
        x: -10 - (ox - i) * 5,
        y: 15 + j * 5,
        z: range.z / 10,
        delay: Math.min(range.delay, 100) / 2 * (i + (oy - j))
      }
    },
    // 排序延時,右下
    orderDelayRB (range, i, j, size) {
      farCamera()
      let [ox, oy] = size
      return {
        x: 10 + i * 5,
        y: -15 - (oy - j) * 5,
        z: range.z / 10,
        delay: Math.min(range.delay, 100) / 2 * ((ox - i) + j)
      }
    },
    // 排序延時, 右上
    orderDelayRT (range, i, j, size) {
      farCamera()
      let [ox, oy] = size
      return {
        x: 10 + i * 5,
        y: 15 + j * 5,
        z: range.z / 10,
        delay: Math.min(range.delay, 100) / 2 * ((ox - i) + (oy - j))
      }
    }
  }
  let changeArr = []
  for (let key in moveType) {
    changeArr.push(key)
  }
  // 類型索引
  let changeIndex = 0
  // 過渡動畫旋轉的隨機數, 0表明旋轉,其餘不進行旋轉
  let rotationIndex = 0
  // 旋轉方向
  let rotationDirection = -1
  // 定時器和動畫中止變量(用於銷燬對象)
  let timer = null
  let stoped = false
  // 當前動畫運行待完成的小方塊數據
  let cubeAniComplete = 0
  export default {
    name: 'Slider3D',
    props: {
      // 圖片
      items: {
        type: Array,
        default: () => ({})
      },
      // 是否預加載圖片
      preload: {
        type: Boolean,
        default: true
      },
      // 是否自動播放
      auto: {
        type: Boolean,
        default: true
      },
      // 是否反向
      reverse: {
        type: Boolean,
        default: false
      }
    },
    data () {
      return {
        // 將canvas的精度放大多少倍,最多不會超過原圖片的精度
        canvasScale: 3,
        // webgl canvas
        canvas: null,
        // 繪圖canvas
        imageCanvas: null,
        // 容器
        wrapper: null,
        // 是否當前資源正在加載中
        cvLoading: true,
        // 當前是否正在動畫中, 防止頻繁切換
        isAnimating: false,
        // webGl canvas尺寸
        cWidth: 0,
        cHeight: 0,
        // 當前圖片尺寸
        iWidth: 0,
        iHeight: 0,
        // 當前場景的橫縱比
        size: [16, 9],
        // 默認橫縱比
        defaultSize: [16, 9],
        // 當前圖片序號
        currentIndex: 0,
        // 動態計算的容器高度
        acHeight: 0,
        // 內置的物體運動相關的控制參數
        hiddenOption: {
          duration: 2000,
          delay: 0,
          easing () {
            return TWEEN.Easing.Quadratic.InOut
          }
        },
        changeOption: {
          duration: 2000,
          delay: 0,
          easing () {
            return TWEEN.Easing.Quadratic.Out
          }
        },
        // 偏移延遲,主要用於進行動畫過渡時,因爲動畫效果須要耗費的額外延時時間將保存到這裏,存最大值
        offsetDelay: 0,
        startOption: {
          duration: 1000,
          delay: 0,
          easing () {
            return TWEEN.Easing.Quadratic.Out
          }
        },
        endOption: {
          duration: 500,
          delay: 0,
          easing () {
            return TWEEN.Easing.Quadratic.In
          }
        },
        // 一次動做完成後通過多少ms進行下一次動做
        spaceTime: 3000,
        // 立方體之間的間隔, 因爲3D視角的緣由,實際上cube之間的距離不必定是設定值那麼寬
        cubeSpace: 0,
        // 當進行場景切換時,每一個立方體進行移動的邊界範圍(在changeTween方法裏有用到)
        cubeChangeRange: {
          useAnimation: true, // 是否進行中間的過渡動畫
          delay: 100, // 隨機延遲時間範圍
          x: 800, // 偏移值基準,下同 (實際使用時不必定會所有用到)
          y: 400,
          z: 100
        }
      }
    },
    computed: {
      // 當前配置下動畫的總持續時間, 自定義每張圖片的切換時間不會超過這個時間
      animateDuration () {
        let change = this.changeOption || {}
        let start = this.startOption || {}
        let end = this.endOption || {}
        return (change.duration || 1000) + (change.delay || 0)
          + (start.duration || 1000) + (start.delay || 0)
          + (end.duration || 1000) + (end.delay || 0) + this.offsetDelay
      }
    },
    created () {},
    mounted () {
      this.canvas = this.$refs.canvas
      this.wrapper = this.$refs.wrapper
      // let [ww, wh] = [window.innerWidth, window.innerHeight]
      let w = this.wrapper.clientWidth
      this.acHeight = w * this.defaultSize[1] / this.defaultSize[0]
      // 進行傳入數據檢測
      this.items.forEach((item) => {
        if (Object.prototype.toString.call(item) !== '[object Object]' && !item.src)
          throw new Error('傳入數據中存在不合法數據')
      })
      // 初始化部分變量
      stoped = false
      changeIndex = 0
      this.currentIndex = 0
      prevImage = null
      drawImages = []
      this.$nextTick(() => {
        this.updateSize()
        this.initCanvas()
        requestAnimationFrame((time) => {
          this.update(time)
        })
        // 圖片一次加載完成
        if (this.preload) {
          meshs = []
          drawImages = []
          prevImage = null
          this._loadImages(() => {
            this.cvLoading = false
            // 切換到第一張圖片
            this.changeImage(0)
          })
          return false
        }
        this.changeImage(0)
      })
    },
    beforeDestroy () {
      stoped = true
      clearTimeout(timer)
      texture && texture.dispose()
      if (material) {
        material.dispose()
        material.map.dispose()
      }
      meshs.forEach((mesh) => {
        mesh.geometry.dispose()
        mesh.material.dispose()
      })
      texture = null
      material = null
      meshs = []
      scene && scene.dispose()
    },
    methods: {
      initCanvas () {
        // 場景
        scene = new THREE.Scene()
        // 渲染器
        renderer = new THREE.WebGLRenderer({
          canvas: this.canvas,
          alpha: true,
          antialias: true,
          logarithmicDepthBuffer: true
        })
        renderer.setClearColor(0xFFFFFF)
        renderer.setSize(this.cWidth * this.canvasScale, this.cHeight * this.canvasScale)
        this.canvas.style.width = this.cWidth + 'px'
        this.canvas.style.height = this.cHeight + 'px'
        // 相機
        camera = new THREE.PerspectiveCamera(60, this.cWidth / this.cHeight, 1, 10000)
        let cH = this.cHeight / 2
        let z = cH * Math.sqrt(3) + offsetZ
        camera.position.set(0, 0, z)
        camera.lookAt(new THREE.Vector3(0, 0, 0))
      },
      updateSize () {
        if (!this.wrapper) return false
        this.cWidth = this.wrapper.clientWidth
        this.acHeight = this.cWidth * 9 / 16
        this.cHeight = this.acHeight
        renderer && renderer.setSize(this.cWidth * this.canvasScale, this.cHeight * this.canvasScale)
        this.canvas.style.width = this.cWidth + 'px'
        this.canvas.style.height = this.cHeight + 'px'
        if (camera) {
          camera.aspect = this.cWidth / this.cHeight
          let cH = this.cHeight / 2
          let z = cH * Math.sqrt(3) + offsetZ
          camera.position.set(0, 0, z)
          camera.lookAt(new THREE.Vector3(0, 0, 0))
          camera.updateProjectionMatrix()
        }
      },
      changeImage (idx) {
        // 當前動畫正在運行的狀況
        if (this.isAnimating) return false
        // 當前資源正在加載的狀況
        if (this.cvLoading) return false
        // 傳入idx是非數字的時候
        if (Number.isNaN(idx * 1) || idx === null) return false
        // 不存在相應對象的狀況
        let item = this.items[idx]
        if (!item || !item.src) return false
        this.currentIndex = idx
        // 若是canvas已存在,且圖片連接一致的狀況下,直接進行切換操做,無需加載
        let canvas = drawImages[idx]
        this.isAnimating = true
        if (canvas && canvas._imgElement && item.src === canvas._imgElement.src) {
          this._initImage()
          return false
        }
        // 不然判斷爲對應圖片未加載或發生變化,須要從新進行加載
        this.cvLoading = true
        this._loadImage(item, idx, () => {
          this.cvLoading = false
          this._initImage()
        })
      },
      update (time) {
        if (stoped) return false
        requestAnimationFrame((time) => {
          this.update(time)
        })
        renderer.render(scene, camera)
        meshs.forEach((item) => {
          let ud = item.userData
          for (let key in ud) {
            if (!key.includes('group')) continue
            ud[key].update(time)
          }
        })
        TWEEN.update(time)
      },
      toNext () {
        let idx = this.currentIndex + 1 >= this.items.length ? 0 : this.currentIndex + 1
        this.changeImage(idx)
      },
      toPrev () {
        let idx = this.currentIndex - 1 < 0 ? this.items.length - 1 : this.currentIndex - 1
        this.changeImage(idx)
      },
      play () {
        if (!this.auto) return false
        timer = setTimeout(() => {
          if (stoped) return false
          this.reverse ? this.toPrev() : this.toNext()
        }, this.spaceTime)
      },
      _getChangeTweenMoveData ({i = 0, j = 0}) {
        let range = this.cubeChangeRange || {}
        if (!range.useAnimation) return { x: 0, y: 0, z: 0, delay: 0 }
        return moveType[changeArr[changeIndex]](range, i, j, this.size)
      },
      _changeUVs (geometry, ux, uy, ox, oy) {
        if (!geometry || !geometry.attributes || !geometry.attributes.uv) return false
        let uvs = geometry.attributes.uv.array
        let saveUVs = [...uvs]
        for (let i = 0; i < uvs.length; i += 2) {
          uvs[i] = (uvs[i] + ox) * ux
          uvs[i + 1] = (uvs[i + 1] + oy) * uy
        }
        return saveUVs
      },
      _initCube () {
        let [x, y] = this.size
        if (x <= 0 || y <= 0) return false
        // 計算每一個立方體的尺寸(注意:用圖片尺寸來計算,不是容器尺寸)
        let [xSize, ySize] = [this.iWidth / x, this.iHeight / y]
        // 複用或初始化紋理
        texture = texture || new THREE.CanvasTexture(this.imageCanvas)
        // 複用或初始化材質
        material = material || new THREE.MeshBasicMaterial({
          map: texture
        })
        // 建立cube
        for (let i = 0; i < x; i++) {
          for (let j = 0; j < y; j++) {
            let geometry = new THREE.BoxBufferGeometry(xSize, ySize, xSize)
            // 若是已存在對應序號的對象,則複用已存在的對象,跳過生成
            if (meshs[i * y + j]) {
              let mesh = meshs[i * y + j]
              mesh.userData.i = i
              mesh.userData.j = j
              mesh.geometry.attributes.uv.array = new Float32Array([...mesh.userData.uvs])
              // 更新uv
              // this._changeUVs(geometry, 1 / x, 1 / y, i, j)
              // mesh.geometry = geometry
              this._changeUVs(mesh.geometry, 1 / x, 1 / y, i, j)
              mesh.geometry.attributes.uv.needsUpdate = true
              let { width, height } = mesh.geometry.parameters
              // 動畫縮放立方體尺寸
              this._animateCube(mesh, xSize / width, ySize / height)
              continue
            }
            // 修改每一個cube的uv偏移和縮放
            let saveUVs = this._changeUVs(geometry, 1 / x, 1 / y, i, j)
            // 生成新立方體,保存序號信息等並隨機調整初始位置
            let mesh = new THREE.Mesh(geometry, material)
            mesh.userData.i = i
            mesh.userData.j = j
            mesh.userData.uvs = [...saveUVs]
            // 保存到立方體隊列便於操做
            meshs[i * y + j] = mesh
            let markX = (Math.random() * 800 >> 0) % 2 === 0 ? -1 : 1
            let markY = (Math.random() * 100 >> 0) % 2 === 0 ? -1 : 1
            mesh.position.set(
              Math.random() * (markX * 200) + (markX * this.cWidth),
              Math.random() * (markY * 200) + (markY * this.cHeight),
              -50 + Math.random() * 100
            )
            scene.add(mesh)
          }
        }
      },
      _animateCube (mesh, scaleX, scaleY) {
        let { x, y } = mesh.scale
        let group = new TWEEN.Group()
        mesh.userData.group5 = group
        let data = { x, y }
        new TWEEN.Tween(data, group)
          .to({
            x: scaleX,
            y: scaleY
          }, 2000)
          .easing(TWEEN.Easing.Quadratic.In)
          .onUpdate(() => {
            mesh.scale.set(data.x, data.y, data.x)
          })
          .start()
      },
      _initPosition () {
        // 獲取尺寸用於每一個立方體偏移位置的計算
        let [xNum, yNum] = this.size
        let [xSize, ySize] = [this.iWidth / xNum, this.iHeight / yNum]
        cubeAniComplete = xNum * yNum
        for (let index = meshs.length - 1; index >= 0; index--) {
          let mesh = meshs[index]
          // 若是出現立方體個數多餘的狀況,將這次圖片顯示中多餘不用的立方體移到屏幕外並隱藏
          if (index >= (xNum * yNum))  {
            this._hideenTween(mesh)
            continue
          }
          // 立方體顯示
          mesh.visible = true
          // 計算位置
          let {i, j} = mesh.userData
          // 偏移計算單位
          let divideX = xNum / 2 - 0.5
          let divideY = yNum / 2 - 0.5
          let offsetX = i - divideX
          let offsetY = j - divideY
          // 計算立方體旋轉的時候在X軸上佔的最大位置
          let rotateXLength = Math.sqrt(2) * xSize
          // 立方體動畫更新
          this._changeTween(mesh, offsetX, offsetY, rotateXLength, xSize, ySize)
        }
      },
      _hideenTween (mesh) {
        let {x, y, z} = mesh.position
        let data = {
          x, y, z
        }
        let group = mesh.userData.group4 || new TWEEN.Group()
        mesh.userData.group4 = group
        let markX = (Math.random() * 400 >> 0) % 2 === 0 ? -1 : 1
        let markY = (Math.random() * 600 >> 0) % 2 === 0 ? -1 : 1
        let opt = this.hiddenOption || {}
        let easing = typeof opt.easing === 'function' ? opt.easing() : (opt.easing || TWEEN.Easing.Quadratic.InOut)
        new TWEEN.Tween(data, group)
          .to({
            x: Math.random() * (markX * 400) + (markX * this.cWidth),
            y: Math.random() * (markY * 300) + (markY * this.cHeight),
            z: -100 + Math.random() * 50
          }, opt.duration || 1000)
          .easing(easing)
          .delay(opt.delay || 0)
          .onUpdate(() => {
            mesh.position.set(data.x, data.y, data.z)
          })
          .onComplete(() => {
            group.removeAll()
            mesh.visible = false
          })
          .start()
      },
      _startTween (mesh, offsetX, offsetY, rotateXLength, xSize, ySize) {
        let {x, y, z} = mesh.position
        let rotation = mesh.rotation
        let group = mesh.userData.group || new TWEEN.Group()
        // 綁定動畫組到對應的對象上
        mesh.userData.group = group
        let data = {
          x, y, z,
          rx: rotation.x,
          ry: rotation.y,
          rz: rotation.z
        }
        // 隨機旋轉
        let targetRotation = {
          rx: rotation.x,
          ry: rotation.y,
          rz: rotation.z
        }
        targetRotation.rx = 0
        targetRotation.ry = 0
        targetRotation.rz = 0
        if (rotationIndex === 0) {
          targetRotation.ry += rotationDirection * Math.PI / 2
        }
        if (rotationIndex === 1) {
          targetRotation.rx += rotationDirection * Math.PI / 2
        }
        let opt = this.startOption || {}
        let easing = typeof opt.easing === 'function' ? opt.easing() : (opt.easing || TWEEN.Easing.Quadratic.Out)
        new TWEEN.Tween(data, group)
          .to({
            x: offsetX * (rotateXLength + 1),
            y: offsetY * (ySize + 1),
            z: -rotateXLength * 2,
            ...targetRotation
          }, opt.duration || 1000)
          .easing(easing)
          .delay(opt.delay || 0)
          .onUpdate(() => {
            mesh.position.set(data.x, data.y, data.z)
            mesh.rotation.x = data.rx
            mesh.rotation.y = data.ry
            mesh.rotation.z = data.rz
          })
          .onComplete(() => {
            group.removeAll()
            // 結束動畫
            this._endTween(mesh, offsetX, offsetY, xSize, ySize)
          })
          .start()
      },
      _endTween (mesh, offsetX, offsetY, xSize, ySize) {
        let {x, y, z} = mesh.position
        let group = mesh.userData.group2 || new TWEEN.Group()
        mesh.userData.group2 = group
        let data = {
          x, y, z
        }
        let opt = this.endOption || {}
        let easing = typeof opt.easing === 'function' ? opt.easing() : (opt.easing || TWEEN.Easing.Quadratic.InOut)
        new TWEEN.Tween(data, group)
          .to({
            x: offsetX * (xSize + this.cubeSpace),
            y: offsetY * (ySize + this.cubeSpace),
            z: 0
          }, opt.duration || 1000)
          .easing(easing)
          .delay(opt.delay || 0)
          .onUpdate(() => {
            mesh.position.set(data.x, data.y, data.z)
          })
          .onComplete(() => {
            group.removeAll()
            this.isAnimating = false
            cubeAniComplete--
            if (cubeAniComplete > 0 ) return false
            this.play()
          })
          .start()
      },
      _changeTween (mesh, offsetX, offsetY, rotateXLength, xSize, ySize) {
        // 這裏主要是在進行下一次圖片切入前,先打亂位置,進行平滑的過渡
        let {x, y, z} = mesh.position
        let rotaion = mesh.rotation
        let data = {
          x, y, z,
          rx: rotaion.x,
          ry: rotaion.y,
          rz: rotaion.z,
          time: 0
        }
        let group = mesh.userData.group3 || new TWEEN.Group()
        mesh.userData.group3 = group
        let {i, j} = mesh.userData
        let moveData = this._getChangeTweenMoveData({i, j})
        let opt = this.changeOption || {}
        let easing = typeof opt.easing === 'function' ? opt.easing() : (opt.easing || TWEEN.Easing.Quadratic.Out)
        let duration = (opt.duration || 1000)
        let delay = (opt.delay || 0) + (moveData.delay || 0)
        this.offsetDelay = Math.max(moveData.delay || 0, this.offsetDelay)
        // 起始
        let source = {...data}
        let target = {
          x: x + moveData.x,
          y: y + moveData.y,
          z: z + moveData.z,
          rx: source.rx + (moveData.rx || 0),
          ry: source.ry + (moveData.ry || 0),
          rz: source.rz + (moveData.rz || 0),
          time: duration
        }
        new TWEEN.Tween(data, group)
          .to(target, duration)
          .easing(easing)
          .delay(delay)
          .onUpdate(() => {
            let res = moveData.count
              ? moveData.count({...source, time: data.time}, {...target})
              : {...data}
            mesh.position.set(res.x, res.y, res.z)
            mesh.rotation.set(res.rx || data.rx, res.ry || data.ry, res.rz || data.rz)
          })
          .onComplete(() => {
            group.removeAll()
            // 還原鏡頭
            restoreCamera()
            // 這裏開始進行下一次圖片切入並還原全部立方體的位置
            this._startTween(mesh, offsetX, offsetY, rotateXLength, xSize, ySize )
          })
          .start()
      },
      _initImage () {
        let item = this.items[this.currentIndex] || null
        if (!item) return false
        // 獲取對應條目的離屏canvas
        let canvas = drawImages[this.currentIndex]
        let context = canvas.getContext('2d')
        this.imageCanvas = canvas
        let { width, height } = canvas
        // 肯定過渡類型
        changeIndex = Math.random() * changeArr.length - 0.1 >> 0
        // 肯定旋轉類型
        rotationIndex = Math.random() * 2 - 0.1 >> 0
        // 肯定旋轉方向
        rotationDirection = (Math.random() * 10 >> 0) % 2 === 0 ? -1 : 1
        // console.log(rotationDirection, rotationIndex)
        // 爲了保證過渡效果,在發生小方塊數量變化的時候一概採用隨機動畫
        if (item.size) {
          if (this.size[0] !== item.size[0] || this.size[1] !== item.size[1]) changeIndex = 0
        }
        else {
          if (this.size[0] !== this.defaultSize[0] || this.size[1] !== this.defaultSize[1])
            changeIndex = 0
        }

        // 獲取橫縱比
        this.size = item.size || this.defaultSize
        // 設定在webGL中的圖片範圍
        this.iHeight = this.cHeight // 與容器等高
        this.iWidth = this.iHeight / height * width // 寬度進行縮放
        // 進行圖片切換的動畫
        this._switchImage({canvas, context, width, height, item})
        // 生成方塊
        this._initCube()
        // 方塊變化動畫
        this._initPosition()
      },
      _switchImage ({canvas, context, width, height, item}) {
        context.clearRect(0, 0, width, height)
        // 當沒有前一張圖片的時候,不用編寫圖片切換的過渡效果
        if (!prevImage) {
          context.save()
          // 從新繪製當前canvas保存的圖像
          context.drawImage(canvas._imgElement, 0, 0)
          context.restore()
          if (material) {
            material.map.needsUpdate = true
          }
          prevImage = canvas._imgElement
          return false
        }
        // 不然進行圖片切換的過渡操做
        let defaultOption = {
          // 動畫方式
          name: 'translate',
          // 運動類型 (L: 左 T: 頂 R: 右 B: 底 C: 中 X: 水平 Y: 縱向 A: 全部方向 2: To)
          type: 'LT2RB',
          // 持續時間
          duration: 3000,
          // 延遲
          delay: 0,
          // 動畫曲線(記得要用TWEEN內置的曲線,不然不識別)
          easing: TWEEN.Easing.Quadratic.InOut
        }
        this._animateImage({
          ...(Object.assign({}, defaultOption, (item.animate || {})))
        }, context, canvas)
      },
      _animateImage (data = {}, context, canvas) {
        if (!context || !canvas || drawImages.length === 0) return false
        let maxDuaration = this.animateDuration
        // 定義一組動畫
        let animation = {}
        // 定義動畫方式
        const tweenAnimate = (start, target, update = function () {}) => {
          let source = {...start}
          let delay = Math.min((data.delay || 0), maxDuaration - 1000)
          let duration = Math.min(data.duration, maxDuaration - delay)
          new TWEEN.Tween(source)
            .to({
              ...target
            }, duration)
            .easing(data.easing)
            .delay(delay)
            .onStart(() => {
              // 更新紋理
              texture = new THREE.CanvasTexture(canvas)
              // 釋放前材質的紋理
              material.map.dispose()
              // 從新綁定紋理到材質
              material.map = texture
            })
            .onUpdate(() => {
              let x = source.offsetX / 100
              let y = source.offsetY / 100
              if (drawImages.length === 0 || !prevImage) return false
              update(x, y)
              if (material) {
                material.map.needsUpdate = true
              }
            })
            .onComplete(() => {
              // 更新上一圖片
              prevImage = canvas._imgElement
            })
            .start()
        }
        // 平移
        animation.translate = (type) => {
          // 前圖片的尺寸
          let piWidth = prevImage.naturalWidth
          let piHeight = prevImage.naturalHeight
          // 當前圖片的尺寸
          let [width, height] = [canvas.width, canvas.height]
          const translate = {
            // 左上到右下
            LT2RB: () => {
              let source = { offsetX: 0, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, (1 - x) * width, (1 - y) * height,
                  width * x, height * y,
                  0, 0, width * x, height * y
                )
                context.restore()
              })
            },
            // 右上到左下
            RT2LB: () => {
              let source = { offsetX: 0, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, (1 - y) * height, width * x, height * y,
                  (1 - x) * width, 0, width * x, height * y
                )
                context.restore()
              })
            },
            // 上到下
            T2B: () => {
              let source = { offsetX: 100, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, (1 - y) * height, width * x, height * y,
                  0, 0, width * x, height * y
                )
                context.restore()
              })
            },
            // 左到右
            L2R: () => {
              let source = { offsetX: 0, offsetY: 100 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, (1 - x) * width, 0, width * x, height * y,
                  0, 0, width * x, height * y
                )
                context.restore()
              })
            },
            // 右到左
            R2L: () => {
              let source = { offsetX: 0, offsetY: 100 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, 0, width * x, height * y,
                  (1 - x) * width, 0, width * x, height * y
                )
                context.restore()
              })
            },
            // 下到上
            B2T: () => {
              let source = { offsetX: 100, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, 0, width * x, height * y,
                  0, (1 - y) * height, width * x, height * y
                )
                context.restore()
              })
            },
            // 右下到左上
            RB2LT: () => {
              let source = { offsetX: 0, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, 0, width * x, height * y,
                  (1 - x) * width, (1 - y) * height, width * x, height * y
                )
                context.restore()
              })
            },
            // 左下到右上
            LB2RT: () => {
              let source = { offsetX: 0, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, (1 - x) * width, 0, width * x, height * y,
                  0, (1 - y) * height, width * x, height * y
                )
                context.restore()
              })
            },
            // 從中心發散
            C2A: () => {
              let source = { offsetX: 0, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                let centerX = width / 2
                let centerY = height / 2
                context.drawImage(
                  canvas._imgElement,
                  0, 0,
                  width, height,
                  centerX - x * width / 2,
                  centerY - y * height / 2,
                  width * x, height * y,
                )
                context.restore()
              })
            },
            // 從中心水平發散
            C2X: () => {
              let source = { offsetX: 0, offsetY: 100 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                let centerX = width / 2
                context.drawImage(
                  canvas._imgElement, 0, 0,
                  centerX * x, height * y,
                  centerX * (1 - x), 0,
                  centerX * x, height * y
                )
                context.drawImage(
                  canvas._imgElement, width * (1 - x / 2), 0,
                  centerX * x, height * y,
                  centerX, 0,
                  centerX * x, height * y
                )
                context.restore()
              })
            },
            // 從中心縱向發散
            C2Y: () => {
              let source = { offsetX: 100, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                let centerY = height / 2
                context.drawImage(
                  canvas._imgElement,
                  0, 0,
                  width * x, centerY * y,
                  0, centerY * (1 - y),
                  width * x, centerY * y
                )
                context.drawImage(
                  canvas._imgElement,
                  0, height * (1 - y / 2),
                  width * x, centerY * y,
                  0, centerY,
                  width * x, centerY * y
                )
                context.restore()
              })
            },
            // 向中心發散
            A2C: () => {
              let source = { offsetX: 0, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                let centerX = width / 2
                let centerY = height / 2
                context.drawImage(
                  canvas._imgElement,
                  (1 - x) * centerX, (1 - y) * centerY, centerX * x, centerY * y,
                  0, 0, centerX * x, centerY * y
                )
                context.drawImage(
                  canvas._imgElement,
                  centerX, (1 - y) * centerY, centerX * x, centerY * y,
                  (1 - x / 2) * width, 0, centerX * x, centerY * y
                )
                context.drawImage(
                  canvas._imgElement,
                  centerX, centerY, centerX * x, centerY * y,
                  (1 - x / 2) * width, (1 - y / 2) * height, centerX * x, centerY * y
                )
                context.drawImage(
                  canvas._imgElement,
                  (1 - x) * centerX, centerY, centerX * x, centerY * y,
                  0, (1 - y / 2) * height, centerX * x, centerY * y
                )
                context.restore()
              })
            },
            // 向中心水平發散
            X2C: () => {
              let source = { offsetX: 0, offsetY: 100 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                let centerX = width / 2
                context.drawImage(
                  canvas._imgElement,
                  centerX *  (1 - x), 0, centerX * x, height * y,
                  0, 0, centerX * x, height * y,
                )
                context.drawImage(
                  canvas._imgElement,
                  centerX, 0, centerX * x, height * y,
                  (1 - x / 2) * width, 0, centerX * x, height * y,
                )
                context.restore()
              })
            },
            // 向中心縱向發散
            Y2C: () => {
              let source = { offsetX: 100, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                let centerY = height / 2
                context.drawImage(
                  canvas._imgElement,
                  0, centerY * (1 - y), width * x, centerY * y,
                  0, 0, width * x, centerY * y,
                )
                context.drawImage(
                  canvas._imgElement,
                  0, centerY, width * x, centerY * y,
                  0, (1 - y / 2) * height, width * x, centerY * y,
                )
                context.restore()
              })
            }
          }
          translate[type]()
        }
        // 覆蓋
        animation.cover = (type) => {
          // 前圖片的尺寸
          let piWidth = prevImage.naturalWidth
          let piHeight = prevImage.naturalHeight
          // 當前圖片的尺寸
          let [width, height] = [canvas.width, canvas.height]
          const cover = {
            // 左上到右下
            LT2RB: () => {
              let source = { offsetX: 0, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, 0, width * x, height * y,
                  0, 0, width * x, height * y
                )
                context.restore()
              })
            },
            // 右上到左下
            RT2LB: () => {
              let source = { offsetX: 0, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, (1 - x) * width, 0, width * x, height * y,
                  (1 - x) * width, 0, width * x, height * y
                )
                context.restore()
              })
            },
            // 上到下
            T2B: () => {
              let source = { offsetX: 100, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, 0, width * x, height * y,
                  0, 0, width * x, height * y
                )
                context.restore()
              })
            },
            // 左到右
            L2R: () => {
              let source = { offsetX: 0, offsetY: 100 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, 0, width * x, height * y,
                  0, 0, width * x, height * y
                )
                context.restore()
              })
            },
            // 右到左
            R2L: () => {
              let source = { offsetX: 0, offsetY: 100 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, (1 - x) * width, 0, width * x, height * y,
                  (1 - x) * width, 0, width * x, height * y
                )
                context.restore()
              })
            },
            // 下到上
            B2T: () => {
              let source = { offsetX: 100, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, (1 - y) * height, width * x, height * y,
                  0, (1 - y) * height, width * x, height * y
                )
                context.restore()
              })
            },
            // 右下到左上
            RB2LT: () => {
              let source = { offsetX: 0, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, (1 - x) * width, (1 - y) * height, width * x, height * y,
                  (1 - x) * width, (1 - y) * height, width * x, height * y
                )
                context.restore()
              })
            },
            // 左下到右上
            LB2RT: () => {
              let source = { offsetX: 0, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, (1 - y) * height, width * x, height * y,
                  0, (1 - y) * height, width * x, height * y
                )
                context.restore()
              })
            },
            // 從中心發散
            C2A: () => {
              let source = { offsetX: 0, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                let centerX = width / 2
                let centerY = height / 2
                context.drawImage(
                  canvas._imgElement,
                  centerX - x * width / 2,
                  centerY - y * height / 2,
                  width * x, height * y,
                  centerX - x * width / 2,
                  centerY - y * height / 2,
                  width * x, height * y,
                )
                context.restore()
              })
            },
            // 從中心水平發散
            C2X: () => {
              let source = { offsetX: 0, offsetY: 100 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                let centerX = width / 2
                context.drawImage(
                  canvas._imgElement,
                  centerX - x * width / 2,
                  0,
                  width * x, height * y,
                  centerX - x * width / 2,
                  0,
                  width * x, height * y,
                )
                context.restore()
              })
            },
            // 從中心縱向發散
            C2Y: () => {
              let source = { offsetX: 100, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                let centerY = height / 2
                context.drawImage(
                  canvas._imgElement,
                  0, centerY - y * height / 2,
                  width * x, height * y,
                  0, centerY - y * height / 2,
                  width * x, height * y,
                )
                context.restore()
              })
            },
            // 向中心發散
            A2C: () => {
              let source = { offsetX: 0, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                let centerX = width / 2
                let centerY = height / 2
                context.drawImage(
                  canvas._imgElement,
                  0, 0, centerX * x, centerY * y,
                  0, 0, centerX * x, centerY * y
                )
                context.drawImage(
                  canvas._imgElement,
                  (1 - x / 2) * width, 0, centerX * x, centerY * y,
                  (1 - x / 2) * width, 0, centerX * x, centerY * y
                )
                context.drawImage(
                  canvas._imgElement,
                  0, (1 - y / 2) * height, centerX * x, centerY * y,
                  0, (1 - y / 2) * height, centerX * x, centerY * y
                )
                context.drawImage(
                  canvas._imgElement,
                  (1 - x / 2) * width, (1 - y / 2) * height, centerX * x, centerY * y,
                  (1 - x / 2) * width, (1 - y / 2) * height, centerX * x, centerY * y
                )
                context.restore()
              })
            },
            // 向中心水平發散
            X2C: () => {
              let source = { offsetX: 0, offsetY: 100 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                let centerX = width / 2
                context.drawImage(
                  canvas._imgElement,
                  0, 0, centerX * x, height * y,
                  0, 0, centerX * x, height * y,
                )
                context.drawImage(
                  canvas._imgElement,
                  (1 - x / 2) * width, 0, centerX * x, height * y,
                  (1 - x / 2) * width, 0, centerX * x, height * y,
                )
                context.restore()
              })
            },
            // 向中心縱向發散
            Y2C: () => {
              let source = { offsetX: 100, offsetY: 0 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                // 繪製當前圖片
                let centerY = height / 2
                context.drawImage(
                  canvas._imgElement,
                  0, 0, width * x, centerY * y,
                  0, 0, width * x, centerY * y,
                )
                context.drawImage(
                  canvas._imgElement,
                  0, (1 - y / 2) * height, width * x, centerY * y,
                  0, (1 - y / 2) * height, width * x, centerY * y,
                )
                context.restore()
              })
            }
          }
          cover[type]()
        }
        // 淡入淡出
        animation.fade = (type) => {
          // 前圖片的尺寸
          let piWidth = prevImage.naturalWidth
          let piHeight = prevImage.naturalHeight
          // 當前圖片的尺寸
          let [width, height] = [canvas.width, canvas.height]
          let fade = {
            // 淡入
            In: () => {
              let source = { offsetX: 0, offsetY: 100 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.clearRect(0, 0, width, height)
                context.save()
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                context.restore()
                context.save()
                context.globalAlpha = x
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, 0, width, height,
                  0, 0, width, height
                )
                context.restore()
              })
            },
            // 淡出
            Out: () => {
              let source = { offsetX: 0, offsetY: 100 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.clearRect(0, 0, width, height)
                context.save()
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, 0, width, height,
                  0, 0, width, height
                )
                context.restore()
                context.save()
                context.globalAlpha = 1 - x
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                context.restore()
              })
            },
            // 淡入淡出
            InOut: () => {
              let source = { offsetX: 0, offsetY: 100 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.clearRect(0, 0, width, height)
                context.save()
                context.globalAlpha = x
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, 0, width, height,
                  0, 0, width, height
                )
                context.restore()
                context.save()
                context.globalAlpha = 1 - x
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                context.restore()
              })
            },
            // 淡出淡入
            OutIn: () => {
              let source = { offsetX: 0, offsetY: 100 }
              let target = { offsetX: 100, offsetY: 100 }
              tweenAnimate(source, target, (x, y) => {
                context.clearRect(0, 0, width, height)
                context.save()
                context.globalAlpha = 1 - x
                // 繪製前圖片
                context.drawImage(
                  prevImage, 0, 0, piWidth, piHeight,
                  0, 0, piWidth, piHeight
                )
                context.restore()
                context.save()
                context.globalAlpha = x
                // 繪製當前圖片
                context.drawImage(
                  canvas._imgElement, 0, 0, width, height,
                  0, 0, width, height
                )
                context.restore()
              })
            }
          }
          fade[type]()
        }
        animation[data.name](data.type)
      },
      // 組圖片加載
      _loadImages (callback = function () {}) {
        let items = this.items || []
        let len = items.length
        // 統計圖片加載動做反饋數
        let num = 0
        if (len === 0) {
          callback()
          return false
        }
        items.forEach((item, index) => {
          this._loadImage(item, index, () => {
            num++
            if (num === len) {
              // 圖片所有加載完成
              callback()
            }
          })
        })
      },
      // 圖片加載主方法
      _loadImage (item, index, callback = function () {}) {
        if (!item) return false
        // 建立對應條目的離屏canvas
        let canvas = drawImages[index] || document.createElement('canvas')
        drawImages[index] = canvas
        let context = canvas.getContext('2d')
        context.clearRect(0, 0, canvas.width, canvas.height)
        // 圖片加載處理
        let img = new Image()
        img.crossOrigin = 'anonymous'
        img.onload = () => {
          let width = img.naturalWidth
          let height = img.naturalHeight
          canvas.width = width
          canvas.height = height
          context.save()
          context.drawImage(img, 0, 0)
          // 將img掛載到對應的canvas下
          canvas._imgElement = img
          context.restore()
          this.cvLoading = false
          callback()
        }
        img.onerror = function () {
          // 加載失敗也進行操做完成的反饋
          callback()
        }
        img.src = item.src
      }
    }
  }
</script>

<style scoped>
  .slider-3d{
    position: relative;
    height: auto;
  }

  .slider-3d-box{
    position: relative;
    height: auto;
  }
</style>

最後的話

以爲好用的能夠交♂易一波,點個收藏或者贊都行,有代碼問題能夠留言,隨緣回答(嗯哼,單身狗就是這麼硬氣( ◔ิω◔ิ) )

相關文章
相關標籤/搜索