Source: lock/lock.js

const LIMIT = 60
let prev = 0

/**
 * Class Lock
 * @class
 * @classdesc 手势解锁组件逻辑部分
 * @author pfan
 * 
 * @example
 *  new Lock(this,{
 *    canvasWidth: 300,   //canvas画布宽度 px
 *    canvasHeight: 300,  //canvas画布高度 px 
 *    canvasId: 'canvasLock', //canvas画布id
 *    drawColor: '#3985ff'  //绘制颜色
 *  })
 */
class Lock {

  /**
   * @constructs Lock构造函数
   * @param  {Object} pageContext page路由指针
   * @param  {Object} opts      组件所需参数
   * @param  {Number} opts.canvasWidth  canvas画布宽度 px
   * @param  {Number} opts.canvasHeight  canvas画布高度 px
   * @param  {String} opts.canvasId  canvas画布id
   * @param  {String} opts.drawColor    绘制颜色
   */  
  constructor (pageContext, opts) {
    this.page = pageContext
    this.canvasWidth = opts.canvasWidth || 300
    this.canvasHeight = opts.canvasHeight || 300
    this.canvasId = opts.id || 'canvasLock'
    this.chooseType = opts.chooseType || 3 //宫格 3x3
    this.drawColor = opts.drawColor || "#3985ff"

    this.init()

    this.page.updatePassword = this.updatePassword.bind(this)
    this.page.onTouchstart = this.onTouchstart.bind(this)
    this.page.onTouchmove = this.onTouchmove.bind(this)
    this.page.onTouchend = this.onTouchend.bind(this)
    this.page.onTouchcancel = this.onTouchcancel.bind(this)
  }

  init() {
      this.setData()   
      this.pswObj = {}

      this.lastPoint = []
      this.touchFlag = false
      this.ctx = wx.createCanvasContext(this.canvasId)
      this.createCircle()
  }

  createCircle() { // 计算各个点坐标,画圆,根据canvas的大小来平均分配半径
      let n = this.chooseType
      let count = 0
      let r = this.r = this.canvasWidth / (2 + 4 * n) // 计算cricle半径
      this.lastPoint = []
      this.arr = []  //记录9宫格位置
      this.restPoint = []
      for (let i = 0 ; i < n ; i++) {
          for (let j = 0 ; j < n ; j++) {
              count++;
              let obj = {
                  x: j * 4 * r + 3 * r,
                  y: i * 4 * r + 3 * r,
                  index: count
              };
              this.arr.push(obj)
              this.restPoint.push(obj)
          }
      }
      this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)

      for (let i = 0 ; i < this.arr.length ; i++) {
          this.drawCle(this.arr[i].x, this.arr[i].y)
      }
      this.ctx.draw(true)
  }

  drawCle(x, y) { // 初始化解锁密码面板
      this.ctx.setStrokeStyle(this.drawColor)
      this.ctx.setLineWidth(2)
      this.ctx.beginPath();
      this.ctx.arc(x, y, this.r, 0, Math.PI * 2, true)
      this.ctx.closePath()
      this.ctx.stroke()
  }

  onTouchstart (e) {
    let po = this.getPosition(e)
    for (let i = 0 ; i < this.arr.length ; i++) {
        if (Math.abs(po.x - this.arr[i].x) < this.r && Math.abs(po.y - this.arr[i].y) < this.r) {
            this.touchFlag = true
            this.drawPoint(this.arr[i].x, this.arr[i].y)
            this.lastPoint.push(this.arr[i])
            this.restPoint.splice(i, 1)
            break;
        }
    }

    this.touchFlag && this.ctx.draw(true) 
  }

  onTouchmove (e) {
      let now = new Date()
      let duration = now - prev
      // 由于小程序canvas效率低下,帧频率大于60丢弃
      if (duration < Math.floor(1000 / LIMIT) ) return;
      prev = now

      if(this.touchFlag){
        this.update(this.getPosition(e))  
      } 
  }

  onTouchend (e) {
      if (this.touchFlag) {
          this.touchFlag = false
          this.storePass(this.lastPoint)

          //300ms 重置,重置时间会影响lastPoint取值, 影响drawline报错
          setTimeout(() => {
              this.reset()
          }, 300)
      }
  }

  onTouchcancel (e) {
    this.touchFlag = false
  }

  getPosition(e) { // 获取touch点相对于canvas的坐标
      return {
          x: e.touches[0].x,
          y: e.touches[0].y
      }
  }

  update(po) { // 核心变换方法在touchmove时候调用
      this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
      for (let i = 0 ; i < this.arr.length ; i++) { // 每帧先把面板画出来
          this.drawCle(this.arr[i].x, this.arr[i].y)
      }

      this.drawPoint(this.lastPoint) // 每帧花轨迹
      this.drawLine(po , this.lastPoint) // 每帧画圆心

      for (let i = 0 ; i < this.restPoint.length ; i++) {
          let pt = this.restPoint[i]

          if (Math.abs(po.x - pt.x) < this.r && Math.abs(po.y - pt.y) < this.r) {
              this.drawPoint(pt.x, pt.y)
              this.pickPoints(this.lastPoint[this.lastPoint.length - 1], pt)
              break;
          }
      }
      this.ctx.draw(true)
  }

  drawPoint() { // 初始化圆心
      for (let i = 0 ; i < this.lastPoint.length ; i++) {
          this.ctx.setFillStyle(this.drawColor)  // 注意用set方法
          this.ctx.beginPath()
          this.ctx.arc(this.lastPoint[i].x, this.lastPoint[i].y, this.r / 2, 0, Math.PI * 2, true)
          this.ctx.closePath()
          this.ctx.fill()
      }
  }

  drawLine(po, lastPoint) { // 解锁轨迹
      this.ctx.beginPath()
      this.ctx.lineWidth = 3
      this.ctx.moveTo(this.lastPoint[0].x, this.lastPoint[0].y)

      for (let i = 1 ; i < this.lastPoint.length ; i++) {
          this.ctx.lineTo(this.lastPoint[i].x, this.lastPoint[i].y)
      }
      this.ctx.lineTo(po.x, po.y)
      this.ctx.stroke()
      this.ctx.closePath()
  }

  pickPoints(fromPt, toPt) {
      let lineLength = getDis(fromPt, toPt)
      let dir = toPt.index > fromPt.index ? 1 : -1

      let len = this.restPoint.length;
      let i = dir === 1 ? 0 : (len - 1)
      let limit = dir === 1 ? len : -1

      while (i !== limit) {
          let pt = this.restPoint[i]

          if ( getDis(pt, fromPt) + getDis(pt, toPt) === lineLength) {
              this.drawPoint(pt.x, pt.y)
              this.lastPoint.push(pt)
              this.restPoint.splice(i, 1)
              if (limit > 0) {
                  i--
                  limit--
              }
          }

          i += dir
      }
  }
  
  storePass(psw) {  // touchend结束之后对密码和状态的处理    
      let title, color
      if (this.pswObj.step == 1) {
          if (this.checkPass(this.pswObj.fpassword, psw)) {
              this.pswObj.step = 2
              this.pswObj.spassword = psw
              title = '密码保存成功'
              color = this.drawColor
              this.drawStatusPoint(this.drawColor)
          } else {
              title = '两次不一致,重新输入'
              color = 'red'
              this.drawStatusPoint('red')
              delete this.pswObj.step
          }
      } else if (this.pswObj.step == 2) {
          if (this.checkPass(this.pswObj.spassword, psw)) {
              title = '解锁成功'
              this.drawStatusPoint(this.drawColor)
          } else {
              title = '解锁失败'
              this.drawStatusPoint('red')
          }
      } else {
          this.pswObj.step = 1
          this.pswObj.fpassword = psw
          title = '再次输入'
      }

      this.setData(title, color)     
  }

  checkPass(psw1, psw2) { // 检测密码
      let p1 = ''
      let p2 = ''
      psw1.forEach( item => {
        p1 += item.index + item.index
      })
      psw2.forEach( item => {
        p2 += item.index + item.index
      })   
      return p1 === p2
  }

  drawStatusPoint(color) { // 初始化状态线条
      for (let i = 0 ; i < this.lastPoint.length ; i++) {
          this.ctx.setStrokeStyle(color)
          this.ctx.beginPath()
          this.ctx.arc(this.lastPoint[i].x, this.lastPoint[i].y, this.r, 0, Math.PI * 2, true)
          this.ctx.closePath()
          this.ctx.stroke()
      }

      this.ctx.draw(true)
  }

  updatePassword() { 
      this.pswObj = {}
      this.setData()
      this.reset()
  }

  setData (title = "绘制解锁图案", color = "#888") {
    let {canvasWidth, canvasHeight} = this   
    this.page.setData({
      lockData: {
        title: title,
        color: color,
        canvasHeight: canvasHeight,
        canvasWidth: canvasWidth
      }
    })    
    if(title == "密码保存成功"){
      setTimeout( () => {
          this.page.setData({
            lockData: {
              title: "开始解锁",
              color: color,
              canvasHeight: canvasHeight,
              canvasWidth: canvasWidth
            }
          })  
      }, 1000)
    }   
  }

  reset() {
      this.createCircle()
  }

}


/**
 * getDis 获取两点直线距离
 * @param  {Object} a 坐标
 * @param  {Object} b 坐标
 * @return {Number}   距离值
 */
function getDis(a, b) {
    return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}

export default Lock