使用 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]);
};

(四) 最终效果

相关推荐
IBELIEVE5 分钟前
前端打包文件本地简易部署
前端
逆袭的小黄鸭7 分钟前
仿 ElementPlus 组件库(九)—— Switch 组件实现
前端·vue.js·typescript
curdcv_po9 分钟前
Vue 项目线上更新无需强制刷新的方案
前端
dchen7720 分钟前
xhr和fetch的一些区别对比
前端·javascript·面试
进击的松鼠22 分钟前
AI 应用多的我眼花缭乱,不妨做个导航试试看
前端·全栈·next.js
徐小夕22 分钟前
耗资100小时,我开源了一款PPT在线编辑器
前端·javascript·github
三原30 分钟前
项目管理:这个bug是你的问题!这个不是bug是需求变更!
前端·后端·团队管理
Vae_Mars30 分钟前
uniapp中props的用法
前端·javascript·uni-app
百事可乐☆31 分钟前
vue 对接 paypal 订阅和支付
前端·javascript·vue.js