<!--
@description: 滑动图片验证码
@author: 肖文龙
@date：  2020-10-29
-->
<template>
  <div id="slideVerify" class="slide-verify" :style="{width: w + 'px'}" onselectstart="return false;">
    <!-- 图片加载遮蔽罩 -->
    <!-- <div :class="{'slider-verify-loading': loadBlock}" /> -->
    <div v-if="loadBlock" class="slider-verify-loading">
      <div class="progress">
        <div class="glimmer" />
        图片加载中...
      </div>
    </div>
    <canvas ref="canvas" :width="w" :height="h" />
    <div v-if="show" class="slide-verify-refresh-icon" @click="refresh" />
    <canvas ref="block" :width="w" :height="h" class="slide-verify-block" />
    <!-- container -->
    <div class="slide-verify-slider" :class="{'container-active': containerActive, 'container-success': containerSuccess, 'container-fail': containerFail}">
      <div class="slide-verify-slider-mask" :style="{width: sliderMaskWidth}">
        <!-- slider -->
        <div
          class="slide-verify-slider-mask-item"
          :style="{left: sliderLeft}"
          @mousedown="sliderDown"
          @touchstart="touchStartEvent"
          @touchmove="touchMoveEvent"
          @touchend="touchEndEvent"
        >
          <div class="slide-verify-slider-mask-item-icon" />
        </div>
      </div>
      <span class="slide-verify-slider-text">{{ sliderText }}</span>
    </div>
  </div>
</template>
<script>
const PI = Math.PI
function sum(x, y) {
  return x + y
}
function square(x) {
  return x * x
}
export default {
  name: 'SlideVerify',
  props: {
    // block length
    l: {
      type: Number,
      default: 40
    },
    // block radius
    r: {
      type: Number,
      default: 7
    },
    // canvas width
    w: {
      type: Number,
      default: 365
    },
    // canvas height
    h: {
      type: Number,
      default: 160
    },
    sliderText: {
      type: String,
      default: 'Slide filled right'
    },
    accuracy: {
      type: Number,
      default: 5 // 若为 -1 则不进行机器判断
    },
    show: {
      type: Boolean,
      default: true
    },
    imgs: {
      type: Array,
      default: () => []
    },
    slidingImagesId: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      containerActive: false, // container active class
      containerSuccess: false, // container success class
      containerFail: false, // container fail class
      canvasCtx: null,
      blockCtx: null,
      block: null,
      block_x: undefined, // container random position
      block_y: undefined,
      L: this.l + this.r * 2 + 3, // block real lenght
      img: undefined,
      originX: undefined,
      originY: undefined,
      isMouseDown: false,
      trail: [],
      sliderLeft: 0, // block right offset
      sliderMaskWidth: 0, // mask width,
      success: false, // Bug Fixes 修复了验证成功后还能滑动
      loadBlock: true, // Features 图片加载提示，防止图片没加载完就开始验证
      timestamp: null
    }
  },
  watch: {
    success(val) {
      if (val) {
        this.$emit('successVerify', this.slidingImagesId)
      }
    }
  },
  mounted() {
    this.init()
  },
  methods: {
    init() {
      this.initDom()
      this.initImg()
      this.bindEvents()
    },
    initDom() {
      this.block = this.$refs.block
      this.canvasCtx = this.$refs.canvas.getContext('2d')
      this.blockCtx = this.block.getContext('2d')
    },
    initImg() {
      const img = this.createImg(() => {
        // 图片加载完关闭遮蔽罩
        this.loadBlock = false
        this.drawBlock()
        this.canvasCtx.drawImage(img, 0, 0, this.w, this.h)
        this.blockCtx.drawImage(img, 0, 0, this.w, this.h)
        const {
          block_x: x,
          block_y: y,
          r,
          L
        } = this
        const _y = y - r * 2 - 1
        const ImageData = this.blockCtx.getImageData(x, _y, L, L)
        this.block.width = L
        this.blockCtx.putImageData(ImageData, 0, _y)
      })
      this.img = img
    },
    drawBlock() {
      this.block_x = this.getRandomNumberByRange(this.L + 10, this.w - (this.L + 10))
      this.block_y = this.getRandomNumberByRange(10 + this.r * 2, this.h - (this.L + 10))
      this.draw(this.canvasCtx, this.block_x, this.block_y, 'fill')
      this.draw(this.blockCtx, this.block_x, this.block_y, 'clip')
    },
    draw(ctx, x, y, operation) {
      const {
        l,
        r
      } = this
      ctx.beginPath()
      ctx.moveTo(x, y)
      ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI)
      ctx.lineTo(x + l, y)
      ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI)
      ctx.lineTo(x + l, y + l)
      ctx.lineTo(x, y + l)
      ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true)
      ctx.lineTo(x, y)
      ctx.lineWidth = 2
      if (operation === 'fill') {
        // 设置或返回用于笔触的颜色、渐变或模式
        ctx.strokeStyle = 'rgba(255, 255, 255, 0)'
        // fillStyle设置或返回用于填充绘画的颜色、渐变或模式
        ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'
        // 设置或返回用于阴影的模糊级别
        ctx.shadowBlur = 4
        // 设置或返回阴影距形状的水平距离
        ctx.shadowOffsetX = 2
        // 设置或返回阴影距形状的垂直距离
        ctx.shadowOffsetY = 3
        // 设置或返回用于阴影的颜色
        ctx.shadowColor = '#fff'
      } else if (operation === 'clip') {
        ctx.strokeStyle = 'rgba(0, 0, 0, 0)'
        ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'
        ctx.shadowOffsetX = 2
        ctx.shadowOffsetY = 3
        ctx.shadowBlur = 3
        // 设置或返回用于阴影的颜色
        ctx.shadowColor = '#000'
      }
      ctx.fill()
      ctx.stroke()
      ctx[operation]()
      // Bug Fixes 修复了火狐和ie显示问题
      ctx.globalCompositeOperation = 'destination-over'
    },
    createImg(onload) {
      const img = document.createElement('img')
      img.crossOrigin = 'Anonymous'
      img.onload = onload
      img.onerror = () => {
        img.src = this.getRandomImg()
      }
      img.src = this.getRandomImg()
      return img
    },
    // 随机生成img src
    getRandomImg() {
      // return require('../assets/img.jpg')
      const len = this.imgs.length
      return len > 0
        ? this.imgs[this.getRandomNumberByRange(0, len)]
        : `https://picsum.photos/seed/${this.getRandomNumberByRange(0, 1084)}/${this.w}/${this.h}`
        // : 'https://picsum.photos/10/20'
    },
    getRandomNumberByRange(start, end) {
      return Math.round(Math.random() * (end - start) + start)
    },
    refresh() {
      this.reset()
      this.$emit('refresh')
    },
    sliderDown(event) {
      if (this.success) return
      this.originX = event.clientX
      this.originY = event.clientY
      this.isMouseDown = true
      this.timestamp = +new Date()
    },
    touchStartEvent(e) {
      if (this.success) return
      this.originX = e.changedTouches[0].pageX
      this.originY = e.changedTouches[0].pageY
      this.isMouseDown = true
      this.timestamp = +new Date()
    },
    bindEvents() {
      document.addEventListener('mousemove', (e) => {
        if (!this.isMouseDown) return false
        const moveX = e.clientX - this.originX
        const moveY = e.clientY - this.originY
        if (moveX < 0 || moveX + 38 >= this.w) return false
        this.sliderLeft = moveX + 'px'
        const blockLeft = (this.w - 40 - 20) / (this.w - 40) * moveX
        this.block.style.left = blockLeft + 'px'
        this.containerActive = true // add active
        this.sliderMaskWidth = moveX + 'px'
        this.trail.push(moveY)
      })
      document.addEventListener('mouseup', (e) => {
        if (!this.isMouseDown) return false
        this.isMouseDown = false
        if (e.clientX === this.originX) return false
        this.containerActive = false // remove active
        this.timestamp = +new Date() - this.timestamp
        const {
          spliced,
          TuringTest
        } = this.verify()
        if (spliced) {
          if (this.accuracy === -1) {
            this.containerSuccess = true
            this.success = true
            this.$emit('success', this.timestamp)
            return
          }
          if (TuringTest) {
            // succ
            this.containerSuccess = true
            this.success = true
            this.$emit('success', this.timestamp)
          } else {
            this.containerFail = true
            this.$emit('again')
          }
        } else {
          this.containerFail = true
          this.$emit('fail')
          setTimeout(() => {
            this.reset()
          }, 1000)
        }
      })
    },
    touchMoveEvent(e) {
      if (!this.isMouseDown) return false
      const moveX = e.changedTouches[0].pageX - this.originX
      const moveY = e.changedTouches[0].pageY - this.originY
      if (moveX < 0 || moveX + 38 >= this.w) return false
      this.sliderLeft = moveX + 'px'
      const blockLeft = (this.w - 40 - 20) / (this.w - 40) * moveX
      this.block.style.left = blockLeft + 'px'
      this.containerActive = true
      this.sliderMaskWidth = moveX + 'px'
      this.trail.push(moveY)
    },
    touchEndEvent(e) {
      if (!this.isMouseDown) return false
      this.isMouseDown = false
      if (e.changedTouches[0].pageX === this.originX) return false
      this.containerActive = false
      this.timestamp = +new Date() - this.timestamp
      const {
        spliced,
        TuringTest
      } = this.verify()
      if (spliced) {
        if (this.accuracy === -1) {
          this.containerSuccess = true
          this.success = true
          this.$emit('success', this.timestamp)
          return
        }
        if (TuringTest) {
          // succ
          this.containerSuccess = true
          this.success = true
          this.$emit('success', this.timestamp)
        } else {
          this.containerFail = true
          this.$emit('again')
        }
      } else {
        this.containerFail = true
        this.$emit('fail')
        setTimeout(() => {
          this.reset()
        }, 1000)
      }
    },
    verify() {
      const arr = this.trail // drag y move distance
      const average = arr.reduce(sum) / arr.length // average
      const deviations = arr.map(x => x - average) // deviation array
      const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length) // standard deviation
      const left = parseInt(this.block.style.left)
      const accuracy = this.accuracy <= 1 ? 1 : this.accuracy > 10 ? 10 : this.accuracy
      return {
        spliced: Math.abs(left - this.block_x) <= accuracy,
        TuringTest: average !== stddev // equal => not person operate
      }
    },
    reset() {
      this.success = false
      this.containerActive = false
      this.containerSuccess = false
      this.containerFail = false
      this.sliderLeft = 0
      this.block.style.left = 0
      this.sliderMaskWidth = 0
      this.loadBlock = true
      // canvas
      const {
        w,
        h
      } = this
      this.canvasCtx.clearRect(0, 0, w, h)
      this.blockCtx.clearRect(0, 0, w, h)
      this.block.width = w
      // 重新获取id
      this.initGetSlidingImagesId()
      // generate img
      this.img.src = this.getRandomImg()
      // this.loadBlock = false
      this.$emit('fulfilled')
    }
  }
}
</script>
<style lang="less" scoped>
    .slide-verify {
        position: relative;
    }
    /* 图片加载样式 */
    .slider-verify-loading {
      text-align:center;
      position: absolute;
      top: 50%;
      left: 50%;
      /*垂直水平居中*/
      margin: -60px 0 0 -40px;
      width: 80px;
      font-size: 10px;
      color: #027bd0;
    }
    .slider-verify-loading >.progress>.glimmer {
      /*垂直水平居中*/
      margin: 0 auto;
      margin-bottom: 10px;
      width: 26px;
      height: 26px;
      border: 2px solid;
      border-color: #027bd0 #027bd0 transparent;
      border-radius: 50%;
      box-sizing: border-box;
      /*动画时间1s，线性变化，无限循环*/
      animation: loading 0.8s linear infinite;
    }

    @keyframes loading {
      0% {
        transform: rotate(0deg);
      }
      100% {
        transform: rotate(360deg);
      }
    .slide-verify-block {
        position: absolute;
        left: 0;
        top: 0
    }
    .slide-verify-refresh-icon {
        position: absolute;
        right: 0;
        top: 0;
        width: 34px;
        height: 34px;
        cursor: pointer;
        background: url("./img/icon_light.png") 0 -437px;
        background-size: 34px 471px
    }
    .slide-verify-slider {
        position: relative;
        text-align: center;
        width: 100%;
        height: 40px;
        line-height: 40px;
        margin-top: 15px;
        background: #f7f9fa;
        color: #45494c;
        border: 1px solid #e4e7eb
    }
    .slide-verify-slider-mask {
        position: absolute;
        left: 0;
        top: 0;
        height: 40px;
        border: 0 solid #1991FA;
        background: #D1E9FE
    }
    .slide-verify-slider-mask-item {
        position: absolute;
        top: 0;
        left: 0;
        width: 40px;
        height: 40px;
        background: #fff;
        box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
        cursor: pointer;
        transition: background .2s linear
    }
    .slide-verify-slider-mask-item:hover {
        background: #1991FA
    }
    .slide-verify-slider-mask-item:hover .slide-verify-slider-mask-item-icon {
        background-position: 0 -13px
    }
    .slide-verify-slider-mask-item-icon {
        position: absolute;
        top: 15px;
        left: 13px;
        width: 14px;
        height: 12px;
        background: url("./img/icon_light.png") 0 -26px;
        background-size: 34px 471px
    }
    .container-active .slide-verify-slider-mask-item {
        height: 38px;
        top: -1px;
        border: 1px solid #1991FA;
    }
    .container-active .slide-verify-slider-mask {
        height: 38px;
        border-width: 1px;
    }
    .container-success .slide-verify-slider-mask-item {
        height: 38px;
        top: -1px;
        border: 1px solid #52CCBA;
        background-color: #52CCBA !important;
    }
    .container-success .slide-verify-slider-mask {
        height: 38px;
        border: 1px solid #52CCBA;
        background-color: #D2F4EF;
    }
    .container-success .slide-verify-slider-mask-item-icon {
        background-position: 0 0 !important;
    }
    .container-fail .slide-verify-slider-mask-item {
        height: 38px;
        top: -1px;
        border: 1px solid #f57a7a;
        background-color: #f57a7a !important;
    }
    .container-fail .slide-verify-slider-mask {
        height: 38px;
        border: 1px solid #f57a7a;
        background-color: #fce1e1;
    }
    .container-fail .slide-verify-slider-mask-item-icon {
        top: 14px;
        background-position: 0 -82px !important;
    }
    .container-active .slide-verify-slider-text,
    .container-success .slide-verify-slider-text,
    .container-fail .slide-verify-slider-text {
        display: none;
    }
</style>
