my-revolve/src/Revolve.vue

<template>
  <div class="my-revolve">
    <canvas :width="width" :height="height" ref="canvas"></canvas>
    <el-progress v-if="!loaded"
                 class="my-revolve__progress"
                 type="circle"
                 :percentage="percentage"></el-progress>
  </div>
</template>

<script>

  /**
   * 3D旋转组件
   * @author chenhuachun
   *
   * @module $ui/components/my-revolve
   *
   */

  import {on, off} from 'element-ui/lib/utils/dom'

  export default {
    name: 'MyRevolve',
    /**
     * 属性参数
     * @member props
     * @property {String[]} [data] 动画帧图片数组
     * @property {number} [width=400] 画布宽度
     * @property {number} [height=500] 画布高度
     * @property {boolean} [auto=false] 自动转动
     * @property {number} [interval=125] 自动旋转速度,每帧的切换时间,值越小越快,
     * @property {boolean} [manual=true] 可鼠标控制转动
     * @property {number} [step=0] 自动转动几帧后停顿,0表示不停顿
     * @property {number} [wait=2000] 停顿的时间,单位ms
     *
     */
    props: {
      data: {
        type: Array,
        default() {
          return []
        }
      },
      width: {
        type: Number,
        default: 400
      },
      height: {
        type: Number,
        default: 500
      },
      // 启动旋转
      auto: Boolean,

      // 手动鼠标控制转
      manual: {
        type: Boolean,
        default: true
      },

      // 旋转速度,值越小越快
      interval: {
        type: Number,
        default: 1000 / 8
      },
      // 转几张后等待时间
      wait: {
        type: Number,
        default: 2000
      },
      // 转几张后暂停等待, 大于0启用
      step: {
        type: Number,
        default: 0
      }
    },
    data() {
      return {
        loaded: false,
        images: [],
        currentIndex: 0,
        progress: 0,
        timer: null,
        delayTimer: null,
        // 手动鼠标控制
        mouseControlX: null,
        mouseControlling: false
      }
    },
    computed: {
      percentage() {
        if (!this.data.length) return 0
        return Math.floor(this.progress * 100 / this.data.length)
      }
    },
    watch: {
      currentIndex() {
        this.draw()
      }
    },
    methods: {
      preload() {
        const total = this.data.length
        if (this.loaded) {
          /**
           * 图片预加载完成时触发
           * @event loaded
           */
          this.$emit('loaded')
          return
        }
        this.data.forEach(n => {
          const img = new Image()
          this.images.push(img)
          img.onload = () => {
            ++this.progress
            /**
             * 预加载图片进度
             * @event progress
             * @param {number} progress 已完成几张
             * @param {number} total 总数
             */
            this.$emit('progress', this.progress, total)
            if (this.progress === total) {
              this.loaded = true
              this.$emit('loaded')
            }
          }
          img.src = n
        })
      },
      draw() {
        const img = this.images[this.currentIndex]
        this.ctx.clearRect(0, 0, this.width, this.height)
        this.ctx.drawImage(img, 0, 0, this.width, this.height);
      },
      delayStart() {
        this.delayTimer && clearTimeout(this.delayTimer)
        this.delayTimer = setTimeout(this.start, this.wait)
      },
      change() {
        if (this.mouseControlling) return
        ++this.currentIndex
        if (this.currentIndex >= this.data.length) {
          this.currentIndex = 0
        }
        /**
         * 帧切换时触发
         * @event change
         * @param {number} currentIndex 帧索引
         */
        this.$emit('change', this.currentIndex)
        if (this.step > 0 && this.currentIndex % this.step === 0) {
          this.stop()
          this.delayStart()
        }

      },
      /**
       * 开始转动
       * @method start
       */
      start() {
        this.timer = setInterval(this.change, this.interval)
        /**
         * 开始转动时触发
         * @event start
         */
        this.$emit('start')
      },
      /**
       * 停止转动
       * @method stop
       */
      stop() {
        this.delayTimer && clearTimeout(this.delayTimer)
        this.timer && clearInterval(this.timer)
        /**
         * 停止转动时触发
         * @event stop
         */
        this.$emit('stop')
      },
      /**
       * 下一帧
       * @method next
       */
      next() {
        ++this.currentIndex
        if (this.currentIndex >= this.data.length) {
          this.currentIndex = 0
        }
      },
      /**
       * 上一帧
       * @method prev
       */
      prev() {
        --this.currentIndex
        if (this.currentIndex < 0) {
          this.currentIndex = this.data.length - 1
        }
      },
      getPointOnCanvas(canvas, x, y) {
        const rect = canvas.getBoundingClientRect();
        return {
          x: x - rect.left * (canvas.width / rect.width),
          y: y - rect.top * (canvas.height / rect.height)
        }
      },
      handleMouseDown(e) {
        const x = e.pageX;
        const y = e.pageY;
        const canvas = event.target;
        const loc = this.getPointOnCanvas(canvas, x, y);
        this.mouseControlX = loc.x
        this.mouseControlling = true
        this.stop()
      },
      handleMouseUp(e) {
        if (!this.mouseControlling) return
        this.mouseControlX = -1
        this.mouseControlling = false
        this.auto && this.start()
      },
      handleMouseMove(e) {
        if (!this.mouseControlling) return
        const x = e.pageX;
        const y = e.pageY;
        const canvas = event.target;
        const loc = this.getPointOnCanvas(canvas, x, y);
        const total = this.data.length
        const diff = 50
        let count = Math.floor(Math.abs((this.mouseControlX - loc.x) / diff));
        let frameIndex = Math.floor((this.mouseControlX - loc.x) / diff);

        while (count > 0) {
          count--
          if (frameIndex > 0) {
            frameIndex--
            this.currentIndex++
          } else if (frameIndex < 0) {
            frameIndex++
            this.currentIndex--
          } else if (frameIndex === 0) {
            break;
          }

          if (this.currentIndex >= total) {
            this.currentIndex = 0
          }
          if (this.currentIndex < 0) {
            this.currentIndex = total - 1
          }

        }

      }
    },
    mounted() {
      const canvas = this.$refs.canvas
      this.ctx = canvas.getContext('2d')
      this.$on('loaded', () => {
        this.draw()
        this.auto && this.start()
      })
      this.preload()
      if (this.manual) {
        on(canvas, 'mousedown', this.handleMouseDown)
        on(canvas, 'mousemove', this.handleMouseMove)
        on(canvas, 'mouseup', this.handleMouseUp)
      }
    },
    beforeDestroy() {
      const canvas = this.$refs.canvas
      this.delayTimer && clearTimeout(this.delayTimer)
      this.stop()
      this.$off('loaded')
      off(canvas, 'mousedown', this.handleMouseDown)
      off(canvas, 'mousemove', this.handleMouseMove)
      off(canvas, 'mouseup', this.handleMouseUp)
    }
  }
</script>