详情讲解canvas实现电子签名

发布时间 2023-08-23 09:22:30作者: 南风晚来晚相识

签名的实现功能

我们要实现签名:
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>