200行代码实现canvas九宫格密码锁

现在很多app,在一些隐私页面,往往都会加入二次验证,例如银行app、支付宝理财和我的页面,一般会有「九宫格密码」和指纹密码。

今天我们用canvas来写一个九宫格手势密码锁,大概就是下面这样。

思路

  1. 准备一个正方形画布
  2. 找到9个小圆圈的圆心坐标(位置自己定,布局合理即可)
  3. 绘制圆圈
  4. 监听手势并连接小圆圈

实现

第一步:先初始化一个空白画布

js 复制代码
<canvas id="canvas"></canvas>


class GesturePassword {
  // 正方形,宽高都一样,就用一个size了
  // padding 画布的边距,百分比
  constructor(canvas, {size = 300, padding = 0.08} = {}) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.size = size; 
    // 计算画布实际的padding大小
    this.padding = size * padding; 
    // 初始化一些属性
    this.init();
  }
  
  init() {
    const { ctx, canvas, size } = this;
    canvas.width = size;
    canvas.height = size;
    // 为了开发时看得清楚,先把背景设为深色
    ctx.fillStyle = "#000";
    ctx.fillRect(0, 0, size, size);
  }
}

第二步:画9个小圆

canvas画圆API

js 复制代码
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
  • x:圆心的 x 轴坐标。
  • y:圆心的 y 轴坐标。
  • radius:圆的半径
  • startAngle:圆弧的起始点,x 轴方向开始计算,单位以弧度表示。
  • endAngle:圆弧的终点,单位以弧度表示。
  • anticlockwise(可选):可选的Boolean值,如果为 true,逆时针绘制圆弧,反之,顺时针绘制。

找圆心坐标和半径

定义函数

js 复制代码
// 计算圆的坐标
calcCirclePos() {
  const { size, padding } = this;
  // 去除画布padding之外的内容宽高
  const contentSize = size - padding * 2; 

  // 除去圆与圆之间的距离
  // 规定每个小圆的直径是总宽度的24%
  const circleWidth = contentSize * 0.24; 
  
  // 每两个圆圈的圆心之间的距离,横竖都一样
  const distance = (contentSize - circleWidth) / 2; 
  
  // 左上角第一个圆的圆心坐标,x和y都一样
  const firstPoint = Math.ff(circleWidth / 2); 
  
  // 综上,第一行三个圆的x轴坐标如下
  const xy = [
    firstPoint,
    Math.ff(firstPoint + distance),
    Math.ff(firstPoint + distance * 2)
  ];

  // 由于横竖每个圆之间的间隔都是一样的,
  // 所以很容易想到,通过以上三个值遍历就可以得出9个圆的圆心
  const points = [];
  let i = 0;
  while (i < 3) {
    for (let index = 0; index < xy.length; index++) {
      const element = xy[index];
      points.push({ x: element, y: xy[i] });
    }
    i++;
  }

  // 最后还要加上padding才是圆心在画布内的真实位置
  return {
    points: points.map((item) => {
      return {
        x: Math.ff(item.x + padding),
        y: Math.ff(item.y + padding)
      };
    }),
    circleWidth
  };
}

Math.ff是为了解决浮点数计算丢失精度问题的

js 复制代码
// 浮点数计算,f代表需要计算的表达式,digit代表小数位数
Math.ff = function(f, digit = 2) {
  // Math.pow(指数,幂指数)
  const m = Math.pow(10, digit);
  // Math.round() 四舍五入
  return Math.round(f * m, 10) / m;
};

在init中调一下

js 复制代码
init() {
  // ...前面的省略了
  // 计算九个圆圈的圆心的坐标和直径大小
  const { points, circleWidth } = this.calcCirclePos();
  // 存起来
  this.points = points;
  this.circleWidth = circleWidth;
}

绘制小圆

定义画圆函数

js 复制代码
drawCircle() {
  const { points, circleWidth, ctx } = this;
  // 循环绘制9个圆
  points.forEach((item, index) => {
    // 每一次都要重新开始新路径
    ctx.beginPath();
    ctx.arc(item.x, item.y, circleWidth / 2, 0, Math.PI * 2);
    ctx.closePath();
    // 将线条颜色设置为蓝色
    ctx.strokeStyle = "#217bfb"; 
    // stroke() 方法默认颜色是黑色(如果没有上面一行,则会是黑色)
    ctx.stroke(); 
  });
}

看看效果

第三步:监听手势

这里要判断一下是什么设备,电脑上就监听mouse事件,手机上就监听touch事件,不过这个效果一般是在手机上用的。

这里有两个辅助函数

  • 计算触摸/鼠标移动到的当前坐标
  • 用拿到的当前坐标,和9个小圆坐标以及圆的半径对比,判断是否滑动到了圆圈内
js 复制代码
const { canvas } = this;
// 判断设备
const isMobile = /Mobile|Android/i.test(navigator.userAgent);

if (isMobile) {
  // 监听触摸开始事件
  canvas.addEventListener(
    "touchstart",
    (e) => {
      // 这里要判断一下是几指触摸,只允许单指触摸
      if (e.touches.length !== 1) return;
      // 获取触摸的坐标位置
      const { x, y } = this.getTouchPosition(canvas, e.touches[0]);
      
      // 判断是否滑动到了圆圈内,是就返回圆的坐标
      const point = this.trigger(x, y);
      console.log("[ this.trigger(x, y) ] >", point);
      
      if (!point) {
        // 没有返回坐标,就说明没有滑到任何一个小圆内,就不用管
        return
      }
      // 把被触发的小圆坐标存起来
      this.hitPoints.push(point);
      // 绘制触发后的样式和连线
      this.drawHitCircle();
    },
    false
  );
  
  // 监听触摸移动事件
  canvas.addEventListener(
    "touchmove",
    (e) => {
      // 防止页面跟着移动
      e.preventDefault();
      if (e.touches.length !== 1) return;
      const { x, y } = this.getTouchPosition(canvas, e.touches[0]);
      const point = this.trigger(x, y);
      console.log("[ this.trigger(x, y) ] >", point);
      if (!point) {
        // 没有返回坐标,就说明没有滑到任何一个小圆内,就不用管
        return
      }
      if (this.hitPoints.includes(point)) {
        // 如果那个位置已被命中过了,就不管
        return
      }
      // 把被触发的小圆坐标存起来
      this.hitPoints.push(point);
      // 绘制触发后的样式和连线
      this.drawHitCircle();
    },
    { passive: false }
  );

  canvas.addEventListener("touchend", async () => {
    if (this.hitPoints.length < 4) {
      setTimeout(() => {
        // 这里用计时器的作用是防止alert阻塞正常逻辑
        alert('密码无效,至少需要四个点')
      }, 0)
    } else {
      // 密码有效将密码传给后端或存起来
      await http()
      // 然后清空临时存储的点
      this.hitPoints = [];
    }
    // 重新绘制
    this.drawHitCircle();
  });
} else {
  // 非手机端,逻辑一致,不同的是监听方法不同
}

定义获取触摸坐标的函数

js 复制代码
getTouchPosition(canvas, event) {
  // 获取画布相对于浏览器窗口的位置信息
  // 当画布不在浏览器左上角时必须这么计算
  const rect = canvas.getBoundingClientRect();
  const x = event.pageX - rect.left;
  const y = event.pageY - rect.top;
  return { x, y };
}

判断是否进入了某个圆圈内

js 复制代码
// 接收触摸位置的坐标 x,y
// 判断手指进入了某个圆圈内,返回圈圈坐标
trigger(x, y) {
  // 先得到被命中的圆圈下标
  const index = this.points.map((item) => {
    const distance = Math.sqrt((x - item.x) ** 2 + (y - item.y) ** 2);
    return distance < this.circleWidth / 2;
  }).findIndex((item) => item);
  
  // 返回该坐标
  return this.points[index];
}

第四步:绘制命中后的样式

遍历之前存的hitPoints坐标数组,将圆环变为蓝色,并在内部画一个小圆填充

js 复制代码
// 绘制命中后的圆圈样式
drawHitCircle() {
  const { hitPoints, ctx } = this;

  console.log("[ hitPoints ] >", hitPoints);
  if (hitPoints.length === 0) {
    // 手指离开画布后会清空坐标,此时清空画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // 但是要重新画圆圈
    drawCircle();
    return;
  }

  hitPoints.forEach((item, index) => {
    ctx.beginPath();
    ctx.arc(item.x, item.y, this.circleWidth / 2, 0, Math.PI * 2);
    ctx.closePath();
    // 将线条颜色设置为蓝色
    ctx.strokeStyle = "#217bfb"; 
    // stroke() 方法默认颜色是黑色(如果没有上面一行,则会是黑色)
    ctx.stroke(); 
    
    // 画小圆要重新开始路径
    ctx.beginPath();
    // 小圆半径设置为大圆半径的1/3
    ctx.arc(item.x, item.y, this.circleWidth / 2 / 3, 0, Math.PI * 2);
    ctx.closePath();
    // 蓝色小圆
    ctx.fillStyle = "#217bfb";
    ctx.fill();
    
    // 从第二个圆开始画一条线连接前后两个圆
    if (index > 0) {
      ctx.beginPath();
      ctx.moveTo(this.hitPoints[index - 1].x, this.hitPoints[index - 1].y);
      ctx.lineTo(item.x, item.y);
      ctx.strokeStyle = "#217bfb";
      ctx.stroke();
    }
  });
}

看看最终效果

还可以再优化的点

  1. 目前的绘制效果有点模糊

❝ 因为 canvas 不是矢量图,而是像图片一样是位图模式的。高 dpi 显示设备意味着每平方英寸有更多的像素。也就是说二倍屏,浏览器就会以 2 个像素点的宽度来渲染一个像素,该 canvas 在 Retina 屏幕下相当于占据了2倍的空间,相当于图片被放大了一倍,因此绘制出来的图片文字等会变模糊。 ❞

解决canvas模糊的问题

  1. 在还没有滑到任何一个小圆内时,页面上没有任何表现,可以加一个跟手的操作,像这样,但是要解决边移动边渲染的性能问题。

有兴趣的可以去实现一下。

相关推荐
漠河愁21 天前
pdf文件渲染到canvas
canvas·pdf.js·fabirc.js
xachary1 个月前
前端使用 Konva 实现可视化设计器(21)- 绘制图形(椭圆)
canvas·konva
x007xyz1 个月前
前端纯手工绘制音频波形图
前端·音视频开发·canvas
甄齐才2 个月前
canvas绘制文本时,该如何处理首行缩进、自动换行、多内容以省略号结束、竖排的呢?
canvas·html2canvas·海报·html转图片·文章分享·dom-to-image·html转image
万水千山走遍TML2 个月前
canvas绘制表格
前端·javascript·vue.js·canvas·canvas绘图·在vue中使用canvas·canvas绘制表格
xachary2 个月前
前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线
javascript·vue·canvas·konva
梦想身高1米82 个月前
canvas.toDataURL后图片背景变成黑色
前端·canvas
x007xyz2 个月前
Fabric.js实时播放视频并扣除绿幕
前端·javascript·canvas
xachary2 个月前
前端使用 Konva 实现可视化设计器(18)- 素材嵌套 - 加载阶段
算法·canvas·konva
LeaferJS2 个月前
LeaferJS 1.0 重磅发布:强悍的前端 Canvas 渲染引擎
前端·canvas