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

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

相关推荐
wayhome在哪1 天前
用 fabric.js 搞定电子签名拖拽合成图片
javascript·产品·canvas
德育处主任1 天前
p5.js 掌握圆锥体 cone
前端·数据可视化·canvas
德育处主任2 天前
p5.js 3D 形状 "预制工厂"——buildGeometry ()
前端·javascript·canvas
德育处主任4 天前
p5.js 3D盒子的基础用法
前端·数据可视化·canvas
掘金安东尼4 天前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
百万蹄蹄向前冲5 天前
让AI写2D格斗游戏,坏了我成测试了
前端·canvas·trae
用户2519162427117 天前
Canvas之画图板
前端·javascript·canvas
FogLetter10 天前
玩转Canvas:从静态图像到动态动画的奇妙之旅
前端·canvas
用户25191624271111 天前
Canvas之贪吃蛇
前端·javascript·canvas
用户25191624271111 天前
Canvas之粒子烟花
前端·javascript·canvas