使用 canvas 实现电子签名

一、引言

电子签名作为数字化身份认证的核心技术之一,已广泛应用于合同签署、审批流程等场景。之前做公司项目时遇到这个需求,于是研究了下,目前前端主要有两种方式实现电子签名:原生Canvason 和 使用signature_pad 依赖库。

本文将基于Vue3 + TypeScript技术栈,深入讲解原生Canvas功能实现方案,并提供完整的可落地代码。

二、原生Canvas实现方案

完整代码:GitHub - seapack-hub/seapack-template: seapack-template框架

实现的逻辑并不复杂,就是使用canvas提供一个画板,让用户通过鼠标或者移动端触屏的方式在画板上作画,最后将画板上的图案生成图片保存下来。

(一) 组件核心结构

需要同时处理 鼠标事件(PC端)触摸事件(移动端),实现兼容的效果。

typescript 复制代码
// PC端 鼠标事件
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', endDrawing);

// 移动端 触摸事件
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', endDrawing);

具体流程:通过状态变量控制绘制阶段:

阶段 触发事件 行为
开始绘制 mousedown 记录起始坐标,标记isDrawing=true
绘制中 mousemove 连续绘制路径(lineTo + stroke)
结束绘制 mouseup 重置isDrawing=false`

代码实现:

html 复制代码
<div class="signature-container">
  <canvas
    ref="canvasRef"
    @mousedown="startDrawing"
    @mousemove="draw"
    @mouseup="endDrawing"
    @mouseleave="endDrawing"
    @touchstart="handleTouchStart"
    @touchmove="handleTouchMove"
    @touchend="endDrawing"
    ></canvas>
  <div class="controls">
    <button @click="clearCanvas">清除</button>
    <button @click="saveSignature">保存签名</button>
  </div>
</div>

(二) 类型和变量

typescript 复制代码
//类型定义
type RGBColor = `#${string}` | `rgb(${number},${number},${number})`
type Point = { x: number; y: number }
type CanvasContext = CanvasRenderingContext2D | null

// 配置
const exportBgColor: RGBColor = '#ffffff' // 设置为需要的背景色

//元素引用
const canvasRef = ref<HTMLCanvasElement | null>(null)
const ctx = ref<CanvasContext>()

//绘制状态
const isDrawing = ref(false)
const lastPosition = ref<Point>({ x: 0, y: 0 })

(三) 绘制逻辑实现

初始化画布

scss 复制代码
//初始化画布
onMounted(() => {
  if (!canvasRef.value) return
  //设置画布大小
  canvasRef.value.width = 800
  canvasRef.value.height = 400

  //获取2d上下文
  ctx.value = canvasRef.value.getContext('2d')
  if (!ctx.value) return

  //初始化 画笔样式
  ctx.value.lineWidth = 2
  ctx.value.lineCap = 'round'
  ctx.value.strokeStyle = '#000' //线条颜色
  // 初始填充背景
  fillBackground(exportBgColor)
})

//填充背景方法
const fillBackground = (color: RGBColor) => {
  if (!ctx.value || !canvasRef.value) return
  ctx.value.save()
  ctx.value.fillStyle = color
  ctx.value.fillRect(0, 0, canvasRef.value.width, canvasRef.value.height)
  ctx.value.restore()
}

获取坐标

将事件坐标转换为 Canvas 内部坐标,兼容滚动偏移

typescript 复制代码
//获取坐标点,将事件坐标转换为 Canvas 内部坐标,兼容滚动偏移
const getCanvasPosition = (clientX: number, clientY: number): Point => {
  if (!canvasRef.value) return { x: 0, y: 0 }

  //获取元素在视口(viewport)中位置
  const rect = canvasRef.value.getBoundingClientRect()
  return {
    x: clientX - rect.left,
    y: clientY - rect.top,
  }
}

// 获取事件坐标
const getEventPosition = (e: MouseEvent | TouchEvent): Point => {
  //TouchEvent 是在支持触摸操作的设备(如智能手机、平板电脑)上,用于处理触摸相关交互的事件对象
  if ('touches' in e) {
    return getCanvasPosition(e.touches[0].clientX, e.touches[0].clientY)
  }
  return getCanvasPosition(e.clientX, e.clientY)
}

开始绘制

将 isDrawing 变量值设置为true,表示开始绘制,并获取当前鼠标点击或手指触摸的坐标。

typescript 复制代码
//开始绘制
const startDrawing = (e: MouseEvent | TouchEvent) => {
  isDrawing.value = true
  const { x, y } = getEventPosition(e)
  lastPosition.value = { x, y }
}

绘制中

每次移动时创建新路径,连接上一个点与当前点。

typescript 复制代码
//绘制中
const draw = (e: MouseEvent | TouchEvent) => {
  if (!isDrawing.value || !ctx.value) return
  //获取当前所在位置
  const { x, y } = getEventPosition(e)
  //开始新路径
  ctx.value.beginPath()
  //移动画笔到上一个点
  ctx.value.moveTo(lastPosition.value.x, lastPosition.value.y)
  //绘制线条到当前点
  ctx.value.lineTo(x, y)
  //描边路径
  ctx.value.stroke()
  //更新最后的位置
  lastPosition.value = { x, y }
}

结束绘制

将 isDrawing 变量设为false,结束绘制

typescript 复制代码
//结束绘制
const endDrawing = () => {
  isDrawing.value = false
}

添加清除和保存方法

typescript 复制代码
//清除签名
const clearCanvas = () => {
  if (!ctx.value || !canvasRef.value) return
  ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
}

//保存签名
const saveSignature = () => {
  if (!canvasRef.value) return
  const dataURL = canvasRef.value.toDataURL('image/png')
  const link = document.createElement('a')
  link.download = 'signature.png'
  link.href = dataURL
  link.click()
}

移动端适配

typescript 复制代码
// 触摸事件处理
const handleTouchStart = (e: TouchEvent) => {
  e.preventDefault();
  startDrawing(e.touches[0]);
};

const handleTouchMove = (e: TouchEvent) => {
  e.preventDefault();
  draw(e.touches[0]);
};

(四) 最终效果

相关推荐
互联网搬砖老肖8 分钟前
Web 架构之负载均衡全解析
前端·架构·负载均衡
sunbyte1 小时前
Tailwind CSS v4 主题化实践入门(自定义 Theme + 主题模式切换)✨
前端·javascript·css·tailwindcss
湛海不过深蓝2 小时前
【css】css统一设置变量
前端·css
程序员的世界你不懂2 小时前
tomcat6性能优化
前端·性能优化·firefox
爱吃巧克力的程序媛2 小时前
QML ProgressBar控件详解
前端
进取星辰2 小时前
21、魔法传送阵——React 19 文件上传优化
前端·react.js·前端框架
wqqqianqian3 小时前
国产linux系统(银河麒麟,统信uos)使用 PageOffice 在线打开Word文件,并用前端对话框实现填空填表
linux·前端·word·pageoffice
BillKu3 小时前
CSS实现图片垂直居中方法
前端·javascript·css
GISer_Jing3 小时前
前端性能优化全攻略:从基础体验到首屏加载的深度实践
前端·javascript·性能优化
pink大呲花3 小时前
深入理解 Vue 全局导航守卫:分类、作用与参数详解
前端·javascript·vue.js