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


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

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

    思路

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

    实现

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

    <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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    第二步:画9个小圆

    canvas画圆API

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

    找圆心坐标和半径

    定义函数

    // 计算圆的坐标
    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
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

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

    // 浮点数计算,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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在init中调一下

    init() {
      // ...前面的省略了
      // 计算九个圆圈的圆心的坐标和直径大小
      const { points, circleWidth } = this.calcCirclePos();
      // 存起来
      this.points = points;
      this.circleWidth = circleWidth;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    绘制小圆

    定义画圆函数

    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(); 
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    看看效果

    第三步:监听手势

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

    这里有两个辅助函数

    • 计算触摸/鼠标移动到的当前坐标
    • 用拿到的当前坐标,和9个小圆坐标以及圆的半径对比,判断是否滑动到了圆圈内
    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 {
      // 非手机端,逻辑一致,不同的是监听方法不同
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    定义获取触摸坐标的函数

    getTouchPosition(canvas, event) {
      // 获取画布相对于浏览器窗口的位置信息
      // 当画布不在浏览器左上角时必须这么计算
      const rect = canvas.getBoundingClientRect();
      const x = event.pageX - rect.left;
      const y = event.pageY - rect.top;
      return { x, y };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    // 接收触摸位置的坐标 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];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

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

    // 绘制命中后的圆圈样式
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    看看最终效果

    还可以再优化的点

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

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

    解决canvas模糊的问题

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

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

  • 相关阅读:
    数据结构与算法-生成树与最小生成树
    无涯教程-JavaScript - INFO函数
    R语言使用plot函数可视化数据散点图,使用log函数对X轴数据和Y轴数据进行对数变换后再进行可视化(log(x),log(y))
    基于微信小程序的沁园健身房预约管理系统设计与实现-计算机毕业设计源码+LW文档
    第七章 解析PyTorch中Hook函数(工具)
    Chapter 1 - 8. Introduction to Congestion in Storage Networks
    【Leetcode】446. Arithmetic Slices II - Subsequence
    Nginx 前端 安装,打包,发布项目到服务 流程
    JS中字符串与ASCII码相互转换,前端如何解决秘钥非明文存储
    Tensorflow2 中对模型进行编译,不同loss函数的选择下输入数据格式需求变化
  • 原文地址:https://blog.csdn.net/xinTianou123/article/details/132769543