详情讲解canvas实现电子签名

简介: 详情讲解canvas实现电子签名

签名的实现功能

我们要实现签名:
1.我们首先要鼠标按下,移动,抬起。经过这三个步骤。
我们可以实现一笔或者连笔。
按下的时候我们需要移动画笔,可以使用 moveTo 来移动画笔。
e.pageX,e.pageY来获取坐标位置
移动的时候我们进行绘制 
ctx.lineTo(e.pageX,e.pageY)   
ctx.stroke()
通过开关flag来判断是否绘制
2.我们可以调整画笔的粗细
3.当我们写错的时候,可以撤回上一步
4.重置整个画板
5.点击保存的时候,可以生成一张图片
6.base64转化为file

实现签名

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
      *{
        padding: 0;
        margin: 0;
      }
      #canvas {
        border: 2px dotted #ccc;
        background-repeat: no-repeat;
        background-size: 80px;
      }
    </style>
</head>
<body>
    <div class="con-box">
      <canvas id="canvas" width="600px" height="400px"></canvas>
      <button id="save-btn" onclick="saveHandler">保存</button>
      <button id="reset-btn" onclick="resetHandler">重置</button>
    </div>
</body>
<script>
// 获取canvas元素的DOM对象 
const canvas=document.getElementById('canvas')
// 获取渲染上下文和它的绘画功能
const ctx= canvas.getContext('2d')
// 笔画内容的颜色,一般为黑色
ctx.strokeStyle='#000'
let flag= false
// 注册鼠标按下事件
canvas.addEventListener('mousedown',e=>{
  console.log('按下',e.pageX,e.pageY)
  flag=true
  // 获取按下的那一刻鼠标的坐标,同时移动画笔
  ctx.moveTo(e.pageX,e.pageY)
})
// 注册移动事件
canvas.addEventListener('mousemove',e=>{
  console.log('移动')
  if(flag){
    // 使用直线连接路径的终点 x,y 坐标的方法(并不会真正地绘制)
    ctx.lineTo(e.pageX,e.pageY)
    // 使用 stroke() 方法真正地画线
    ctx.stroke()
  }
})
// 注册抬起事件
canvas.addEventListener('mouseup',e=>{
  console.log('抬起')
  flag=false
})
</script>
</html>

鼠标移入canvas就会触发事件

通过上面的图,我们发现了一个点。
那就是鼠标移入canvas所在的区域。
就会触发移动事件的代码。
这是为什么呢?
因为我们在移入的时候注册了事件,因此就会触发。
现在我们需要优化一下:将移动事件,抬起事件放在按下事件里面
同时,当鼠标抬起的时候,移除移动事件和抬起事件。【不移除按下事件】
这里可能有的小伙伴会问?
为什么抬起的时候不移除按下事件。
因为:代码从上往下执行,当我们移除抬起事件后,我们只能绘画一次了。
当我们移除事件时,我们就不需要开关 flag 了。
删除flag的相关代码
<script>
// 获取canvas元素的DOM对象 
const canvas=document.getElementById('canvas')
// 获取渲染上下文和它的绘画功能
const ctx= canvas.getContext('2d')
// 笔画内容的颜色,一般为黑色
ctx.strokeStyle='#000'
// 注册鼠标按下事件
canvas.addEventListener('mousedown',mousedownFun)
// 按下事件
function mousedownFun(e){
  console.log('按下',e.pageX,e.pageY)
  // 获取按下的那一刻鼠标的坐标,同时移动画笔
  ctx.moveTo(e.pageX,e.pageY)
  // 注册移动事件
  canvas.addEventListener('mousemove',mousemoveFun)
  // 注册抬起事件
  canvas.addEventListener('mouseup',mouseupFun)
}
// 移动事件
function mousemoveFun(e){
  console.log('移动')
  // 使用直线连接路径的终点 x,y 坐标的方法(并不会真正地绘制)
  ctx.lineTo(e.pageX,e.pageY)
  // 使用 stroke() 方法真正地画线
  ctx.stroke()
}
// 抬起事件
function mouseupFun(e){
  console.log('抬起')
  // 移除移动事件
  canvas.removeEventListener('mousemove', mousemoveFun)
  // 移除抬起事件
  canvas.removeEventListener('mouseup', mouseupFun)
}
</script>

发现bug-鼠标不按下也可以绘制笔画

我们发现鼠标移出canvas所在区域后。
然后在移入进来,鼠标仍然可以进行绘制。(此时鼠标已经是松开了)
这很明显是一个bug。这个bug产生的原因在于:
鼠标移出canvas所在区域后没有移出移动事件
// 鼠标移出canvas所在的区域事件-处理鼠标移出canvas所在区域后
// 然后移入不按下鼠标也可以绘制笔画
canvas.addEventListener('mouseout',e=>{
  // 移除移动事件
  canvas.removeEventListener('mousemove', mousemoveFun)
})

如何设置画笔的粗细

我们想要调整画笔的粗细。
需要使用 ctx.lineWidth 属性来设置画笔的大小默认是1。
我们用   <input type="range" class="range" min="1" max="30" value="1" id="range"> 
来调整画笔。
因为我们我们调整画笔后,线条的大小就会发生改变。
因此我们在每次按下的时候都需要开始本次绘画。
抬起的时候结束本次绘画,
这样才能让不影响上一次画笔的大小。
核心的代码
<input type="range" class="range" min="1" max="30" value="1" id="range">
// 获取设置画笔粗细的dom元素
let range = document.querySelector("#range");
// 获取渲染上下文和它的绘画功能
const ctx= canvas.getContext('2d')
// 按下事件
function mousedownFun(e){
  console.log('按下',e.pageX,e.pageY)
  // 开始本次绘画(与画笔大小设置有关)
  ctx.beginPath();
  // 设置画笔的粗细
  ctx.lineWidth = range.value || 1
}
// 抬起事件
function mouseupFun(e){
  // 结束本次绘画(与画笔大小设置有关)
  ctx.closePath();
  console.log('抬起')
}

撤回上一步

1. 先声明一个数组. let historyArr=[]
  按下的时候记录当前笔画起始点的特征(颜色 粗细 位置)
  currentPath = {
    color: ctx.strokeStyle,
    width: ctx.lineWidth,
    points: [{ x: e.offsetX, y: e.offsetY }]
  }
2.按下移动的时候记录每一个坐标点[点连成线]
currentPath.points.push({ x: e.offsetX, y: e.offsetY });
3.鼠标抬起的时候说明完成了一笔(连笔)
  historyArr.push(currentPath);
4.点击撤销按钮的时候删除最后一笔
5.然后重新绘制之前存储的画笔
<!-- 核心代码 -->
<button id="revoke">撤销</button>
let historyArr = [] //保存所有的操作
let currentPath = null;
let revoke=document.querySelector("#revoke");
// 按下事件
function mousedownFun(e){
  // 开始本次绘画(与画笔大小设置有关)
  ctx.beginPath();
  // 设置画笔的粗细
  ctx.lineWidth = range.value || 1
  // 获取按下的那一刻鼠标的坐标,同时移动画笔
  ctx.moveTo(e.pageX,e.pageY)
  // 记录当前笔画起始点的特征(颜色 粗细 位置)
  currentPath = {
    color: ctx.strokeStyle,
    width: ctx.lineWidth,
    points: [{ x: e.offsetX, y: e.offsetY }]
  }
}
// 移动事件
function mousemoveFun(e){
  ctx.lineTo(e.pageX,e.pageY)
  currentPath.points.push({ x: e.offsetX, y: e.offsetY });
  ctx.stroke()
}
// 抬起事件
function mouseupFun(e){
  historyArr.push(currentPath);
  ctx.closePath();
}
// 撤销按钮点击事件
revoke.addEventListener('click', e => {
  if (historyArr.length === 0) return;
  // 删除最后一条的记录
  historyArr.pop()
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawPaths(historyArr);
});
// 画所有的路径
function drawPaths(paths) {
  paths.forEach(path => {
    ctx.beginPath();
    ctx.strokeStyle = path.color;
    ctx.lineWidth = path.width;
    ctx.moveTo(path.points[0].x, path.points[0].y);
    // path.points.slice(1) 少画 与  path.points 区别是少画一笔和正常笔数
    console.log('path',path)
    path.points.slice(1).forEach(point => {
      ctx.lineTo(point.x, point.y);
    });
    ctx.stroke();
  });
}

重置整个画布

<button id="reset" >重置</button>
// 重置整个画布
reset.addEventListener('click',e=>{
  //清空整个画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);
})
ps:清空画布的主要运用了ctx.clearRect这个方法

保存

保存图片主要是通过 canvas.toDataURL 生成的是base64
然后通过a标签进行下载
saveBtn.addEventListener('click',()=>{
  let imgURL = canvas.toDataURL({format: "image/png", quality:1, width:600, height:400});
  let link = document.createElement('a');
  link.download = "tupian";
  link.href = imgURL;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
})

生成file文件发送给后端

// base64转化为file文件
function base64changeFile (urlData, fileName) {
  // split将按照','字符串按照,分割成一个数组,
  // 这个数组通常包含了数据类型(MIME type)和实际的数据。
  // 数组的第1项是类型 第2项是数据
  const arr = urlData.split(',')
  // data:image/png;base64
  const mimeType = arr[0].match(/:(.*?);/)[1]
  console.log('类型',mimeType)
  // 将base64编码的数据转换为普通字符串
  const bytes = atob(arr[1])
  let n = bytes.length
  // 创建了一个新的Uint8Array对象,并将这些字节复制到这个对象中。
  const fileFormat = new Uint8Array(n)
  while (n--) {
    fileFormat[n] = bytes.charCodeAt(n)
  }
  return new File([fileFormat], fileName, { type: mimeType })
}
fileBtn.addEventListener('click',()=>{
  let imgURL = canvas.toDataURL({format: "image/png", quality:1, width:600, height:400});
  let file = base64changeFile(imgURL,'qianMing')
  console.log('file',file)
})

全部代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
      *{
        padding: 0;
        margin: 0;
      }
      #canvas {
        border: 2px dotted #ccc;
      }
    </style>
</head>
<body>
    <div class="con-box">
      <canvas id="canvas" width="600px" height="400px"></canvas>
      <input type="range" class="range" min="1" max="30" value="1" id="range">
      <button id="revoke">撤销</button>
      <button id="save-btn">保存</button>
      <button id="file">转化为file</button>
      <button id="reset" >重置</button>
    </div>
</body>
<script>
// 获取canvas元素的DOM对象 
const canvas=document.getElementById('canvas')
// 获取设置画笔粗细的dom元素
let range = document.querySelector("#range");
let revoke=document.querySelector("#revoke");
let reset=document.querySelector("#reset");
let saveBtn=document.querySelector("#save-btn");
let fileBtn=document.querySelector("#file");
// 获取渲染上下文和它的绘画功能
const ctx= canvas.getContext('2d')
// 笔画内容的颜色,一般为黑色
ctx.strokeStyle='#000'
let historyArr = [] //保存所有的操作
let currentPath = null;
// 注册鼠标按下事件
canvas.addEventListener('mousedown',mousedownFun)
// 按下事件
function mousedownFun(e){
  console.log('按下',e.pageX,e.pageY)
  // 开始本次绘画(与画笔大小设置有关)
  ctx.beginPath();
  // 设置画笔的粗细
  ctx.lineWidth = range.value || 1
  // 获取按下的那一刻鼠标的坐标,同时移动画笔
  ctx.moveTo(e.pageX,e.pageY)
  // 记录当前笔画起始点的特征(颜色 粗细 位置)
  currentPath = {
    color: ctx.strokeStyle,
    width: ctx.lineWidth,
    points: [{ x: e.offsetX, y: e.offsetY }]
  }
  // 注册移动事件
  canvas.addEventListener('mousemove',mousemoveFun)
  // 注册抬起事件
  canvas.addEventListener('mouseup',mouseupFun)
}
// 移动事件
function mousemoveFun(e){
  console.log('移动')
  // 使用直线连接路径的终点 x,y 坐标的方法(并不会真正地绘制)
  ctx.lineTo(e.pageX,e.pageY)
  // 记录画笔的移动的每一个坐标位置
  currentPath.points.push({ x: e.offsetX, y: e.offsetY });
  // 使用 stroke() 方法真正地画线
  ctx.stroke()
}
// 抬起事件
function mouseupFun(e){
  // 一笔结束后存储起来
  historyArr.push(currentPath);
  console.log('historyArr',historyArr)
  // 结束本次绘画(与画笔大小设置有关)
  ctx.closePath();
  console.log('抬起')
  // 移除移动事件
  canvas.removeEventListener('mousemove', mousemoveFun)
  // 移除抬起事件
  canvas.removeEventListener('mouseup', mouseupFun)
}
// 鼠标移出canvas所在的区域事件-处理鼠标移出canvas所在区域后,然后移入不按下鼠标也可以绘制笔画
canvas.addEventListener('mouseout',e=>{
  // 移除移动事件
  canvas.removeEventListener('mousemove', mousemoveFun)
})
  // 撤销按钮点击事件
revoke.addEventListener('click', e => {
  if (historyArr.length === 0) return;
  // 删除最后一条的记录
  historyArr.pop()
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawPaths(historyArr);
});
// 重置整个画布
reset.addEventListener('click',e=>{
  //清空整个画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);
})
// 保存为图片
saveBtn.addEventListener('click',()=>{
  let imgURL = canvas.toDataURL({format: "image/png", quality:1, width:600, height:400});
  console.log('imgURL',imgURL)
  let link = document.createElement('a');
  link.download = "tupian";
  link.href = imgURL;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
})
// 画所有的路径
function drawPaths(paths) {
  console.log(11,paths)
  paths.forEach(path => {
    ctx.beginPath();
    ctx.strokeStyle = path.color;
    ctx.lineWidth = path.width;
    ctx.moveTo(path.points[0].x, path.points[0].y);
    // path.points.slice(1) 少画 与  path.points 区别是少画一笔和正常笔数
    console.log('path',path)
    path.points.slice(1).forEach(point => {
      ctx.lineTo(point.x, point.y);
    });
    ctx.stroke();
  });
}
// base64转化为file文件
function base64changeFile (urlData, fileName) {
  // split将按照','字符串按照,分割成一个数组,
  // 这个数组通常包含了数据类型(MIME type)和实际的数据。
  // 数组的第1项是类型 第2项是数据
  const arr = urlData.split(',')
  // data:image/png;base64
  const mimeType = arr[0].match(/:(.*?);/)[1]
  console.log('类型',mimeType)
  // 将base64编码的数据转换为普通字符串
  const bytes = atob(arr[1])
  let n = bytes.length
  // 创建了一个新的Uint8Array对象,并将这些字节复制到这个对象中。
  const fileFormat = new Uint8Array(n)
  while (n--) {
    fileFormat[n] = bytes.charCodeAt(n)
  }
  return new File([fileFormat], fileName, { type: mimeType })
}
fileBtn.addEventListener('click',()=>{
  let imgURL = canvas.toDataURL({format: "image/png", quality:1, width:600, height:400});
  let file = base64changeFile(imgURL,'qianMing')
  console.log('file',file)
})
</script>
</html>

遇见问题,这是你成长的机会,如果你能够解决,这就是收获。

相关文章
HTML+VUE+element-ui通过点击不同按钮展现不同页面
HTML+VUE+element-ui通过点击不同按钮展现不同页面
|
前端开发 Java
layui结合ajax实现下拉菜单联动效果
layui结合ajax实现下拉菜单联动效果
|
10月前
|
人工智能 JavaScript 前端开发
一段 JavaScript 代码,集成网站AI语音助手
根据本教程,只需通过白屏化的界面操作,即可快速构建一个专属的AI智能体。
|
9月前
|
Python
云产品评测|分布式Python计算服务MaxFrame获奖名单公布!
云产品评测|分布式Python计算服务MaxFrame获奖名单公布!
191 0
|
前端开发 JavaScript
判断数组为空的方法有哪些?
本文介绍了多种判断数组是否为空的方法,包括使用 `length` 属性、隐式类型转换、`toString()`、`join()`、`every()`、`reduce()`、`filter()`、`some()` 方法以及循环。每种方法都有其适用场景,其中使用 `length` 属性和隐式类型转换最为常见和简单。文章首发于微信公众号“前端徐徐”。
921 2
判断数组为空的方法有哪些?
|
JSON 程序员 C#
使用 C# 比较两个对象是否相等的7个方法总结
比较对象是编程中的一项基本技能,在实际业务中经常碰到,比如在ERP系统中,企业的信息非常重要,每一次更新,都需要比较记录更新前后企业的信息,直接比较通常只能告诉我们它们是否指向同一个内存地址,那我们应该怎么办呢?分享 7 个方法给你!
457 2
|
监控 前端开发 安全
C#一分钟浅谈:文件上传与下载功能实现
【10月更文挑战第2天】在Web应用开发中,文件的上传与下载是常见需求。本文从基础入手,详细讲解如何在C#环境下实现文件上传与下载。首先介绍前端表单设计及后端接收保存方法,使用`&lt;input type=&quot;file&quot;&gt;`与`IFormFile`接口;接着探讨错误处理与优化策略,如安全性验证和路径管理;最后讲解文件下载的基本步骤,包括确定文件位置、设置响应头及发送文件流。此外,还提供了进阶技巧,如并发处理、大文件分块上传及进度监控,帮助开发者构建更健壮的应用系统。
716 15
|
小程序 PHP
微信小程序给 thinkphp后端发送请求出现错误 Wrong number of segments 问题的解决 【踩坑记录】
本文记录了微信小程序向ThinkPHP后端发送请求时出现"Wrong number of segments"错误的解决方法。问题原因是小程序请求header中的token变量名写错,导致token未正确传递至后端。作者提供了详细的检查步骤和建议,包括验证URL路径、参数规范和路由配置的匹配,以确保请求能正确发送和处理。