在网页里直接手写签名,早已不是什么新鲜事。无论是OA系统的审批确认,还是电子合同平台在线签署,都离不开一个能在浏览器中流畅书写的签名板。要实现这个功能,html5提供的Canvas是最合适的载体:它既能在PC端响应鼠标操作,也能在手机、平板上响应触摸事件,配合JavaScript就能把用户的手写轨迹实时绘制出来。这篇文章从零开始,带你完整实现一个可嵌入任意页面的电子签名板。

一、Canvas绘图原理与页面结构搭建
Canvas是html5新增的画布元素,它提供了一个矩形绘图区域,所有绘制操作都通过getContext('2d')返回的上下文对象完成。手写签名的本质就是不断记录鼠标或手指经过的坐标点,再用lineTo把相邻的点连接成线段。当点足够密集时,这些线段在视觉上就形成了一条平滑的笔画。
先来看最基础的页面结构。一个容器包裹画布,画布下方放置清除和保存两个按钮,这是签名板最经典的布局。需要注意的是,不要用CSS去拉伸Canvas的尺寸,而是应该通过JS动态设置它的宽高属性,否则绘制出来的线条会出现模糊或变形。
<div class="signature-box"> <canvas id="signCanvas"</canvas> </div> <button id="clearBtn">清除重写</button> <button id="saveBtn">保存签名</button>
画布尺寸自适应的写法如下,利用offsetWidth和offsetHeight获取容器的实际渲染尺寸,再赋值给Canvas的width和height属性,这样能保证画布逻辑尺寸与显示尺寸一致,避免锯齿。
function resizeCanvas() {
const canvas = document.getElementById('signCanvas');
const box = canvas.parentElement;
// 逻辑尺寸与显示尺寸保持一致,避免模糊
canvas.width = box.clientWidth;
canvas.height = box.clientHeight;
}
window.addEventListener('load', resizeCanvas);
window.addEventListener('resize', resizeCanvas);二、事件监听与坐标换算:让PC和手机都能写
签名板必须同时支持鼠标和触摸两种输入。PC端使用mousedown、mousemove、mouseup三个事件,移动端则要监听touchstart、touchmove、touchend。触摸事件返回的是touches数组,取第一个触点即可。此外,移动端书写时页面会跟着滚动,必须在事件里调用preventDefault()阻止默认行为,否则写到一半页面就滑走了。
另一个关键点是坐标换算。事件对象中的clientX和clientY是相对于浏览器视口的坐标,而Canvas绘图使用的是画布内部坐标系,两者必须通过getBoundingClientRect()换算,否则签名位置会和手指位置对不上。
function getPos(canvas, e) {
const rect = canvas.getBoundingClientRect();
const point = e.touches ? e.touches[0] : e;
return {
x: point.clientX - rect.left,
y: point.clientY - rect.top
};
}
const canvas = document.getElementById('signCanvas');
const ctx = canvas.getContext('2d');
let drawing = false;
canvas.addEventListener('mousedown', startDraw);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', endDraw);
canvas.addEventListener('mouseleave', endDraw);
canvas.addEventListener('touchstart', startDraw);
canvas.addEventListener('touchmove', draw);
canvas.addEventListener('touchend', endDraw);
function startDraw(e) {
e.preventDefault();
drawing = true;
const p = getPos(canvas, e);
ctx.beginPath();
ctx.moveTo(p.x, p.y);
}
function draw(e) {
if (!drawing) return;
e.preventDefault();
const p = getPos(canvas, e);
ctx.lineWidth = 3; // 笔画粗细
ctx.lineCap = 'round'; // 线段端点为圆形
ctx.lineJoin = 'round'; // 转角圆润
ctx.strokeStyle = '#000';
ctx.lineTo(p.x, p.y);
ctx.stroke();
}
function endDraw() {
drawing = false;
}这里有个容易被忽略的细节:lineCap和lineJoin设置成round能让笔画连接处圆润自然,如果保持默认的butt和miter,快速书写时会出现明显的折角和断点,观感会很差。
三、笔画平滑处理与签名导出
上面的实现已经可以写字,但快速书写时笔画容易呈现折线感。改善的方法是引入贝塞尔曲线:不直接用lineTo连接点,而是以上一点和当前点的中点作为曲线端点,用quadraticCurveTo绘制二次贝塞尔曲线,笔迹会顺滑许多。
let lastX = 0, lastY = 0;
function smoothDraw(e) {
if (!drawing) return;
e.preventDefault();
const p = getPos(canvas, e);
// 中点法平滑:以两点中点为端点画二次贝塞尔
const midX = (lastX + p.x) / 2;
const midY = (lastY + p.y) / 2;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.quadraticCurveTo(lastX, lastY, midX, midY);
ctx.stroke();
lastX = p.x;
lastY = p.y;
}签名写完后通常需要保存。Canvas提供了toDataURL方法,可以把画布内容导出为png或jpg格式的Base64字符串。png支持透明背景,适合叠加到合同文件上;jpg体积更小但背景不透明。导出时建议先创建一个白底画布把签名复制过去,避免透明背景在某些查看器中显示异常。
document.getElementById('saveBtn').onclick = function () {
// 检查画布是否为空白
if (canvas.toDataURL() === emptyCanvasDataURL()) {
alert('请先书写签名');
return;
}
const exportCanvas = document.createElement('canvas');
exportCanvas.width = canvas.width;
exportCanvas.height = canvas.height;
const ectx = exportCanvas.getContext('2d');
ectx.fillStyle = '#fff';
ectx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
ectx.drawImage(canvas, 0, 0);
const base64 = exportCanvas.toDataURL('image/png');
// 将base64提交给后端存储,或下载到本地
console.log(base64);
};
document.getElementById('clearBtn').onclick = function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
};判断画布是否为空的小技巧是:初始化完成后先调用一次toDataURL记录空白画布的字符串,保存时与当前值比较,相同则说明用户还没写。Base64字符串可以直接通过表单或AJAX提交到后端,存入数据库后再配合业务流程生成签章图片。
四、常见问题与成熟方案选择
自己实现签名板虽然代码量不大,但真要在生产环境使用,还有不少坑要填:一是高分辨率屏幕上的模糊问题,需要按devicePixelRatio放大画布再缩小绘制;二是横竖屏切换时画布内容会被清空,需要在resize前先把内容暂存到图片再恢复;三是iOS Safari对触摸事件的特殊行为,有时需要在touchmove里显式设置passive: false才能生效。
如果项目周期紧张,也可以直接使用成熟的开源库,比如signature_pad,它内置了笔画压感模拟、平滑算法和高分屏适配,API也很简洁:
const canvas = document.querySelector('canvas');
const pad = new SignaturePad(canvas, {
backgroundColor: 'rgb(255,255,255)',
penColor: 'rgb(0,0,0)'
});
// 导出png
const data = pad.toDataURL('image/png');
// 清空
pad.clear();总体来说,理解了Canvas事件监听、坐标换算和曲线平滑这几个核心点,无论是自己造轮子还是使用第三方库,都能做到心中有数。自己实现的好处是代码可控、无额外依赖,第三方库的优势在于兼容性处理更完善,按项目实际情况选择即可。最后提醒一句,网页端的签名图片只解决采集问题,如果要具备法律效力,还需要后端配合数字证书、时间戳等技术手段,这才是完整的电子签名方案。