04月18, 2019

canvas手势解锁

alt

这样的界面在手机端比较常见。下面我就说说实现这个界面的一些思路和代码吧!

前置知识

canvas的基础:

  • 画直线
  • 画圆
  • 画矩形
  • 填充与描边
  • 图形变换(平移、旋转、缩放)
  • 渐变(线性渐变、径向渐变)
  • 文字
  • 图片
  • 剪辑区域
  • 阴影绘制
  • 曲线绘制(圆弧、贝塞尔曲线)
  • 动画
  • 离屏技术

对于要实现的界面来说,其实不需要那么多,只要会以下几个就行:

  • 画圆
  • 画直线
  • canvas事件监听

实现

思路

  • 动态创建文字和canvas(当然也可以一开始就写好布局)
  • 创建九个圆(当然也可以是16个圆,这个可以通过参数配置)
  • 绑定事件(touchstart/touchmove/touchend)

创建九个圆

这个主要是计算圆心的坐标。那么做法也可以分成以下几种:

  • 指定半径
  • 不指定半径

指定半径

那么要计算出来grap,圆与圆的间隙。这个是要用来判断当手指touchstart时,是否在圆内。

const raduis = pointBorderRaduis + pointBorderWidth;
const width = this.width - 2 * raduis;
const height = this.height - 2 * raduis;
const gap = Math.min(width, height) / 2;

不指定半径

  • grap = 2倍的半径,然后左右没有grap
  • grap = 2倍的半径,然后左右有grap

alt

实现代码

这里实现的代码是不指定半径,左右有grap的版本。

class CanvasLock() {
  constructor({ chooseType = 3, width = 300, height = 300 }) {
    this.chooseType = chooseType; // 默认一行3个
    this.width = width;
    this.height = height;
  }
  init() {
    // this.initDom(); // 动态创建canvas
    this.canvas = document.getElementById('canvas');
    this.ctx = this.canvas.getContext('2d');
    this.touchFlag = false; // 是否能拖
    this.createCircle();
    // this.bindEvent(); // 绑定事件
  }
  // 绘制九宫格或16宫格
  createCircle() {
    const n = this.chooseType;
    let count = 0;
    this.r = this.canvas.width / (2 + 4 * n);// 公式计算
    this.arr = [];
    const r = this.r;
    for (let i = 0; i < n; i++) {
      for (let j = 0; j < n; j++) {
        count++;
        this.arr.push({
          x: j * 4 * r + 3 * r,
          y: i * 4 * r + 3 * r,
          index: count
        });
      }
    }
    // 清除画布
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    for (let i = 0; i < this.arr.length; i++) {
      // 画圆函数
      this.drawCle(this.arr[i].x, this.arr[i].y);
    }
  }
  drawCle(x, y) {
    this.ctx.strokeStyle = '#CFE6FF';
    this.ctx.lineWidth = 2;
    this.ctx.beginPath();
    this.ctx.arc(x, y, this.r, 0, Math.PI * 2, true);
    this.ctx.stroke();
  }
}

事件绑定

class CanvasLock { 
    // ...
    bindEvent() {
    this.canvas.addEventListener("touchstart", (e) => {
      // start代码
    }, false);

    this.canvas.addEventListener("touchmove", (e) => {
      // touchmove做的就是:画圆drawPoint和划线drawLine
      if (this.touchFlag) {
        // move代码
      }
    }, false);

    this.canvas.addEventListener("touchend", function (e) {
      if (this.touchFlag) {
        // end代码
      }
    }, false);
    }
    // ...
}

事件start代码

其实要做的事情很简单,就是去判断当前手指的位置是不是在圆内,在的话,设置touchFlag为true。

仅仅设置touchFlag是不够的,还需要将当前的圆记录一下(放到数组里面),同时也要把该圆标记为不可以再次连接

// 得到当前手指的坐标
getPosition(e) {
  const rect = e.currentTarget.getBoundingClientRect();
  const po = {
    x: e.touches[0].clientX - rect.left,
    y: e.touches[0].clientY - rect.top
  };
  return po;
}

代码

// po有x和y,并且是相较于canvas边距
const po = this.getPosition(e); // 获取当前鼠标的坐标
// 判断是否在圆内的原理:多出来的这条 x/y < r 在圆内
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) < self.r) {
    this.touchFlag = true;
    // lastPoint存放的就是选中的圆圈的x/y坐标值
    this.lastPoint.push(this.arr[i]);
    this.restPoint.splice(i, 1);
    break;
  }
}

这里说一下this.lastPoint,初始值为[],记录的目的是为了最后连线有一个编号,如:1234。(在创建圆时,记录的索引值是从1开始的,并不是0)。

然后this.restPoint用来记录还没有被二次连接的圆。它的初始值等同this.arr

alt

事件move代码

if (this.touchFlag) {
  this.update(this.getPosition(e));
}
update(po) {
  this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);

  // 重新画9个圆圈
  for (let i = 0; i < this.arr.length; i++) { // 每帧先把面板画出来
    this.drawCle(this.arr[i].x, this.arr[i].y);
  }

  this.drawPoint();// 画圆
  this.drawLine(po);// 画线

  // 1、检测手势移动的位置是否处于下一个圆内
  // 2、圆内的话则画圆 drawPoint
  // 3、已经画过实心圆的圆,无需重复检测
  for (let i = 0; i < this.restPoint.length; i++) {
    if (Math.abs(po.x - this.restPoint[i].x) < this.r && Math.abs(po.y - this.restPoint[i].y) < this.r) {
      this.drawPoint();
      this.lastPoint.push(this.restPoint[i]);
      this.restPoint.splice(i, 1);
      break;
    }
  }
}
// 画小圆
drawPoint() {
  for (let i = 0; i < this.lastPoint.length; i++) {
    this.ctx.fillStyle = '#CFE6FF';
    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) {
  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();
}

事件end代码

这里要做的事有:

  • 根据得到的路径(比如123)和绘制的路径是否一致,做出不一样的处理结果

alt

alt

  • 解锁失败,要重置状态

重置状态代码

reset() {
  this.createCircle();
}

判断是否和绘制的一样

checkPass() {
  const p1 = '123'; // 假设这是结果
  const p2 = this.lastPoint.map((item) => item.index).join("");
  return p1 === p2;
}

根据状态,让边框变绿或者变红

storePass() {
  if (this.checkPass()) {
    // document.getElementById('title').innerHTML = '解锁成功';
    this.drawStatusPoint('#2CFF26');
  } else {
    // document.getElementById('title').innerHTML = '解锁失败';
    this.drawStatusPoint('red');
  }
}
drawStatusPoint(type) {
  for (let i = 0; i < this.lastPoint.length; i++) {
    this.ctx.strokeStyle = type;
    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();
  }
}

结语

在react/vue的年代,似乎canvas用的很少了,但其实则不然,在图形领域用的还是挺多的,包括像什么d3threejs

我个人是觉得图形这一块比较难,但如果努力去学,未来的发展应该比只会vue/react的要好很多很多。

本文链接:www.my-fe.pub/post/canvas-gesture-unlock.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。