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

news2025/1/21 9:40:36

现在很多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);
  }
}

第二步:画9个小圆

canvas画圆API

ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
  • 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
  };
}

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;
};

在init中调一下

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

绘制小圆

定义画圆函数

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个小圆坐标以及圆的半径对比,判断是否滑动到了圆圈内
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 {
  // 非手机端,逻辑一致,不同的是监听方法不同
}

定义获取触摸坐标的函数

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

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

// 接收触摸位置的坐标 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坐标数组,将圆环变为蓝色,并在内部画一个小圆填充

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/991209.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

四、子向父传值,展示项目经验

简介 发布订阅消息,子向父传值,展示项目经验详细信息。欢迎访问个人的简历网站预览效果 本章涉及修改与新增的文件:Fifth.vue、App.vue、utils、Project.ts 一、创建项目数据 在 src 目录下新建一个 utils 文件夹 ,再创建一个 Project.ts 文件 // 项目经验的详细数据 c…

day56补

583. 两个字符串的删除操作 力扣题目链接(opens new window) 给定两个单词 word1 和 word2&#xff0c;找到使得 word1 和 word2 相同所需的最小步数&#xff0c;每步可以删除任意一个字符串中的一个字符。 示例&#xff1a; 输入: "sea", "eat"输出: …

手撕 队列

队列的基本概念 只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出 入队列&#xff1a;进行插入操作的一端称为队尾 出队列&#xff1a;进行删除操作的一端称为队头 队列用链表实现 队列的实现 队列的定义 队列…

Java(三)逻辑控制(if....else,循环语句)与方法

逻辑控制&#xff08;if....else&#xff0c;循环语句&#xff09;与方法 四、逻辑控制1.if...else(常用)1.1表达格式&#xff08;三种&#xff09; 2.switch...case(用的少)2.1表达式 3.while(常用)3.1语法格式3.2关键字beak&#xff1a;3.3关键字 continue&#xff1a; 4.for…

RedisJava基础代码实现

Jedis快速入门 <!--jedis--> <dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>3.7.0</version> </dependency> <!--单元测试--> <dependency><groupId>org.ju…

华为云云耀云服务器L实例评测|用docker搭建frp服务测试

华为云云耀云服务器L实例评测&#xff5c;用docker搭建frp服务测试 0. 环境 华为云耀云L实例EulerOS 1. 安装docker 检查yum源&#xff0c;本EulerOS的源在这里&#xff1a; cd /etc/yum.repos.d 更新源 yum makecache 安装 yum install -y docker-engine 运行测试 d…

【数据库事务日志碎片原理分析与方案】-深入解析篇.pdf

日志增长与 VLF 文件的个数 通过上面的相关内容的介绍&#xff0c;我们已经知道了日志文件自动的增长会到了一些问 题&#xff0c;而事实确实如此&#xff0c;下面&#xff0c;我们就来更加清楚的看看这些问题。 很显然&#xff0c;我们不希望日志文件任意的增长&#xff0c;…

2020年12月 C/C++(三级)真题解析#中国电子学会#全国青少年软件编程等级考试

C/C编程&#xff08;1~8级&#xff09;全部真题・点这里 第1题&#xff1a;完美立方 形如 a^3 b^3 c^3 d^3的等式被称为完美立方等式。例如 12^3 6^3 8^3 10^3 。 编写一个程序&#xff0c;对任给的正整数 N (N≤100)&#xff0c;寻找所有的四元组 (a, b, c, d)&#xff0c…

leetcode 1382. 将二叉搜索树变平衡

2023.9.8 本题分为两步&#xff0c;先用中序遍历将二叉搜索树转化为排序数组&#xff0c;再通过排序数组构建一个平衡二叉树。 代码如下&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;*…

数据结构与算法基础-学习-32-选择排序之简单选择排序、堆排序

目录 一、简单选择排序基本思路 二、简单选择排序基本操作 三、简单选择排序算法思路 四、简单选择排序代码 1、SimpleSelectSortSentrySqQueue 五、简单选择排序算法分析 1、记录移动次数 2、记录比较次数 六、简单选择排序Linux环境编译测试 七、堆的定义 八、堆调…

MySQL数据表的约束

数据表约束&#xff1a;对于某一列的值能添加哪些内容做了一定的限制&#xff0c;这种限制的手段就称为约束。 &#xff08;一&#xff09;约束的类型 NOT NULL 指示某列不能存储 NULL 值。UNIQUE保证某列的每行必须有唯一的值。DEFAULT规定没有给列赋值时的默认值。PRIMARY …

利用less实现多主题切换(配合天气现象)

1. 先看效果&#xff1a; 2. 话不多说直接撸吧&#xff1a; 原理&#xff1a;先给body元素添加style&#xff0c;再根据天气现象动态更改style 开撸&#xff1a; 创建src/assets/style/variables.less 使用 XXX:var(–XXX,‘style’) 声明系列变量&#xff0c;之后添加其他变…

redis如何保证接口的幂等性

背景 如何防止接口中同样的数据提交&#xff0c;以及如何保证消息不被重复消费&#xff0c;这些都是shigen在学习的过程中遇到的问题。今天&#xff0c;趁着在学习redis的间隙&#xff0c;我写了一篇文章进行简单的实现。 注意&#xff1a;仅使用于单机的场景&#xff0c;对于…

春秋云镜 CVE-2017-1000480

春秋云镜 CVE-2017-1000480 Smarty < 3.1.32 PHP代码执行漏洞 靶标介绍 3.1.32 之前的 Smarty 3 在未清理模板名称的自定义资源上调用 fetch() 或 display() 函数时容易受到 PHP 代码注入的影响。 启动场景 漏洞利用 poc /index.php?eval*/phpinfo();/*/index.php?ev…

原生JavaScript+PHP多图上传实现

摘要 很多场景下需要选择多张图片上传&#xff0c;或者是批量上传以提高效率&#xff0c;多图上传的需求自然就比较多了&#xff0c;本文使用最简单的XMLHttpRequest异步上传图片。 界面 上传示例 代码 index.html <!DOCTYPE html> <html><head><titl…

node.js下载安装环境配置以及快速使用

目录 一、下载 二、安装 三、测试安装是否成功 四、配置环境 五、测试配置环境是否成功 六、安装淘宝镜像 七、快速上手 1、建立一个自己的工作目录 2、下载工作代码 八、各种配置文件匹配问题入坑 九、总结 一、下载 Node.js 中文网 想选择其他版本或者其他系统使用…

【Chrome】chrome浏览器未连接到互联网

问题描述 电脑上安装了一个联想电脑管家&#xff0c;进行了一下清理&#xff0c;并优化了一下启动项&#xff0c;Chrome浏览器突然什么网站都无法访问了。以为更新坏了&#xff0c;但相同的网站放到火狐浏览器上&#xff0c;竟然可以打开&#xff0c;怎么回事呢&#xff1f;怎…

使用EMgu检测人脸

1,安装EMgu 在NuGet中,查找并安装EMgu 2,做人脸检测 首先,声明几个重要的类 //Thread.Sleep(3000);matImg = new Mat();capture.Retrieve(matImg, 0); frame=new Image<Bgr, byte>(matImg.Bitmap); 当,frame != null时,检测到人脸 3,给人脸画框 i…

MySQL主从分离读写复制

在高负载的生产环境里&#xff0c;把数据库进行读写分离&#xff0c;能显著提高系统的性能。下面对MySQL的进行读写分离。 试验环境 A机&#xff1a;IP:192.168.0.1 mysql版本&#xff1a;mysql-5.6.4,主数据服务器&#xff08;只写操作&#xff09; B机&#xff1a;IP:192.…

SpringMVC_执行流程

四、SpringMVC执行流程 1.SpringMVC 常用组件 DispatcherServlet&#xff1a;前端控制器&#xff0c;用于对请求和响应进行统一处理HandlerMapping&#xff1a;处理器映射器&#xff0c;根据 url/method可以去找到具体的 Handler(Controller)Handler:具体处理器&#xff08;程…