使用 canvas 实现电子签名
一、引言
电子签名作为数字化身份认证的核心技术之一,已广泛应用于合同签署、审批流程等场景。之前做公司项目时遇到这个需求,于是研究了下,目前前端主要有两种方式实现电子签名:原生Canvason 和 使用signature_pad 依赖库。
本文将基于Vue3 + TypeScript技术栈,深入讲解原生Canvas功能实现方案,并提供完整的可落地代码。
二、原生Canvas实现方案
完整代码:GitHub - seapack-hub/seapack-template: seapack-template框架
实现的逻辑并不复杂,就是使用canvas提供一个画板,让用户通过鼠标或者移动端触屏的方式在画板上作画,最后将画板上的图案生成图片保存下来。
(一) 组件核心结构
需要同时处理 鼠标事件(PC端) 和 触摸事件(移动端),实现兼容的效果。
// 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` |
代码实现:
<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>
(二) 类型和变量
//类型定义
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 })
(三) 绘制逻辑实现
初始化画布
//初始化画布
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 内部坐标,兼容滚动偏移
//获取坐标点,将事件坐标转换为 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,表示开始绘制,并获取当前鼠标点击或手指触摸的坐标。
//开始绘制
const startDrawing = (e: MouseEvent | TouchEvent) => {
isDrawing.value = true
const { x, y } = getEventPosition(e)
lastPosition.value = { x, y }
}
绘制中
每次移动时创建新路径,连接上一个点与当前点。
//绘制中
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,结束绘制
//结束绘制
const endDrawing = () => {
isDrawing.value = false
}
添加清除和保存方法
//清除签名
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()
}
移动端适配
// 触摸事件处理
const handleTouchStart = (e: TouchEvent) => {
e.preventDefault();
startDrawing(e.touches[0]);
};
const handleTouchMove = (e: TouchEvent) => {
e.preventDefault();
draw(e.touches[0]);
};
(四) 最终效果
作者:烈风逍遥
来源:juejin.cn/post/7484987385665011762
来源:juejin.cn/post/7484987385665011762