一、前言
在日常网站开发中,很多场景都需要用到电子签名功能,比如在线表单、合同签署、报名系统、后台确认签字等。
传统的图片上传签名方式繁琐又不美观,而使用前端 Canvas 实现手写签名,无需后端接口、纯前端实现、轻量化无依赖,同时完美兼容电脑鼠标和手机触屏,体验堪比专业签名工具。
分享一套我封装好的完整版 Canvas 电子签名组件,代码简洁、注释详细、开箱即用。
二、功能亮点
本次实现的签名功能,集成了日常开发所需的全部核心能力,适配多端、体验顺滑:
- 双端兼容:支持电脑鼠标绘制、手机触屏手写,无适配bug
- 顺滑笔迹:圆角画笔处理,模拟真实签字笔书写效果,无锯齿、无卡顿
- 自定义笔迹:支持黑、蓝、红三种常用签名颜色一键切换
- 基础核心操作:一键清空画布、一键导出PNG透明底签名图片
- 防干扰优化:禁止画布区域滚动,解决移动端手写偏移、滑动冲突问题
- 零依赖:原生HTML+CSS+JS开发,不依赖任何框架,直接打开即可运行
三、完整源码(开箱即用)
以下是完整可运行代码,直接复制保存为 .html 文件,或嵌入博客、项目页面中即可使用。代码经过优化,兼容所有现代浏览器。
html
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<!-- 移动端完美适配 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Canvas 电子签名工具</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft Yahei", system-ui, sans-serif;
}
body {
padding: 30px 20px;
max-width: 800px;
margin: 0 auto;
background-color: #f5f7fa;
}
h3 {
color: #333;
margin-bottom: 16px;
font-size: 20px;
}
/* 签名画布容器 */
.sign-box {
border: 2px solid #e5e7eb;
border-radius: 10px;
overflow: hidden;
background: #fff;
touch-action: none; /* 禁止移动端滚动干扰手写 */
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
}
#signCanvas {
display: block;
background: #ffffff;
cursor: crosshair;
}
/* 按钮操作区域 */
.btn-group {
margin-top: 20px;
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
button {
padding: 10px 24px;
border: none;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
transition: all 0.2s ease;
}
button:hover {
opacity: 0.9;
transform: translateY(-2px);
}
.clear-btn {
background-color: #f53f3f;
color: #fff;
}
.save-btn {
background-color: #2b7de9;
color: #fff;
}
/* 颜色选择区域 */
.color-wrap {
display: flex;
align-items: center;
gap: 10px;
margin-left: 10px;
}
.color-text {
color: #666;
font-size: 14px;
}
.color-item {
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
border: 2px solid transparent;
transition: 0.2s;
}
.color-item.active {
border-color: #222;
transform: scale(1.1);
}
</style>
</head>
<body>
<h3>在线电子签名(鼠标/触屏均可手写)</h3>
<div class="sign-box">
<canvas id="signCanvas" width="750" height="400"></canvas>
</div>
<div class="btn-group">
<button class="clear-btn" id="clearBtn">清空签名</button>
<button class="save-btn" id="saveBtn">保存签名图片</button>
<div class="color-wrap">
<span class="color-text">笔迹颜色:</span>
<div class="color-item active" style="background:#000" data-color="#000"></div>
<div class="color-item" style="background:#0984e3" data-color="#0984e3"></div>
<div class="color-item" style="background:#d63031" data-color="#d63031"></div>
</div>
</div>
<script>
// 获取画布元素和上下文
const canvas = document.getElementById('signCanvas');
const ctx = canvas.getContext('2d');
// 全局状态变量
let isDrawing = false; // 是否正在绘制
let lastX = 0; // 上一次绘制X坐标
let lastY = 0; // 上一次绘制Y坐标
let strokeColor = "#000"; // 默认笔迹颜色
const lineWidth = 3; // 固定笔迹粗细
// 初始化画笔样式(顺滑圆角笔迹)
function initCanvasStyle() {
ctx.lineCap = "round"; // 线条两端圆角
ctx.lineJoin = "round"; // 线条连接处圆角
ctx.lineWidth = lineWidth;
ctx.strokeStyle = strokeColor;
}
initCanvasStyle();
// 统一获取鼠标/触屏坐标(修正画布偏移,解决错位问题)
function getCanvasPos(e) {
const rect = canvas.getBoundingClientRect();
// 兼容移动端触屏事件
if (e.touches) {
return {
x: e.touches[0].clientX - rect.left,
y: e.touches[0].clientY - rect.top
}
}
// 兼容电脑鼠标事件
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
// 开始绘制
function startDraw(e) {
e.preventDefault();
isDrawing = true;
const pos = getCanvasPos(e);
lastX = pos.x;
lastY = pos.y;
}
// 绘制过程
function drawing(e) {
if (!isDrawing) return;
e.preventDefault();
const pos = getCanvasPos(e);
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
// 更新坐标,实现连续绘制
lastX = pos.x;
lastY = pos.y;
}
// 停止绘制
function stopDraw() {
isDrawing = false;
}
// 绑定鼠标事件(电脑端)
canvas.addEventListener('mousedown', startDraw);
canvas.addEventListener('mousemove', drawing);
canvas.addEventListener('mouseup', stopDraw);
canvas.addEventListener('mouseout', stopDraw);
// 绑定触屏事件(手机/平板端)
canvas.addEventListener('touchstart', startDraw);
canvas.addEventListener('touchmove', drawing);
canvas.addEventListener('touchend', stopDraw);
// 清空画布功能
document.getElementById('clearBtn').addEventListener('click', () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
});
// 保存签名为PNG图片
document.getElementById('saveBtn').addEventListener('click', () => {
const imgUrl = canvas.toDataURL('image/png');
const aTag = document.createElement('a');
aTag.href = imgUrl;
aTag.download = '个人签名.png';
aTag.click();
});
// 切换笔迹颜色
document.querySelectorAll('.color-item').forEach(item => {
item.addEventListener('click', () => {
// 移除所有选中状态
document.querySelectorAll('.color-item').forEach(el => el.classList.remove('active'));
// 添加当前选中状态
item.classList.add('active');
// 更新画笔颜色
strokeColor = item.dataset.color;
ctx.strokeStyle = strokeColor;
})
})
</script>
</body>
</html>
四、代码核心原理讲解
- 画布样式优化
通过 lineCap: round 和 lineJoin: round 将画笔设置为圆角样式,解决原生 Canvas 绘制线条锯齿、生硬的问题,完美模拟手写真实效果。同时设置 touch-action: none 杜绝移动端滑动页面和手写冲突的bug。 - 双端坐标兼容
单独封装了坐标获取函数,同时适配鼠标事件和触屏事件,并且通过 getBoundingClientRect() 获取画布真实偏移位置,彻底解决画布绘制错位、坐标偏移的常见问题。 - 核心绘制逻辑
通过 mousedown/touchstart 触发绘制开始,记录初始坐标;mousemove/touchmove 实时连线绘制;mouseup/touchend 终止绘制,实现连续手写效果。 - 图片导出原理
利用 Canvas 原生 API toDataURL() 将画布内容转为图片 Base64 链接,通过动态创建 a 标签实现一键下载,无需后端参与,纯前端完成导出。