JavaScript Canvas 手写签名实现:基础绘制、撤销重写与图片导出
本文详细介绍如何使用 JavaScript 和 Canvas 实现手写签名功能。内容涵盖基础绘制逻辑、鼠标事件监听、笔画数据的二维数组存储以支持撤销和重写操作,以及利用 toDataURL 将签名导出为 PNG 图片的完整流程。
预览效果

若无需阅读详细过程,可直接参考源码部分。
首先实现核心需求,即具备基本的签名绘制能力。
梳理实现思路。
第一步,初始化一个 canvas 画布,并获取其 2D 上下文对象(context)。
第二步,配置画笔样式,包括线条宽度和颜色。
第三步,监听鼠标或手指在画布上的移动事件,获取每次移动的坐标点并进行记录。
第四步,将这些记录的坐标点连接绘制到画布上,从而形成可视化的线条。
<canvas width="600" height="400" id="canvas" style="background-color: #ddd;"></canvas>为便于调试,本文示例仅展示 PC 端操作逻辑。移动端实现原理相同,主要区别在于监听的事件 API 不同(如 touch 事件)。
常规交互逻辑为:当鼠标左键按下并在画布上移动时进行绘制;若未按下鼠标,则忽略移动操作。
代码中已添加详细注释,便于理解每一行逻辑。
window.onload = function () {
// 默认鼠标是没有按下的
let isDown = false;
// 记录上一次鼠标的位置
let lastX = 0; // x轴
let lastY = 0; // y轴
// 获取canvas
const canvas = document.getElementById("canvas");
// 获取canvas的上下文
const ctx = canvas.getContext("2d");
// 定义线条的宽度,即画笔的粗细
ctx.lineWidth = 3;
// 定义画笔的颜色
ctx.strokeStyle = "#000";
/**
* 定义绘制方法
* 线条其实是由两个点连起来的一个线段
* 一个又一个的小线段,连起来就组成了一个线条
* 在画布上绘制线条,主要用到的三个核心方法
* moveTo: 是 Canvas 2D API 将一个新的子路径的起始点移动到 (x,y) 坐标的法。
* lineTo: 是 Canvas 2D API 使用直线连接子路径的终点到 x,y 坐标的法。
* 当然,定义了起点和终点还不够,还需要手动调用开始绘制这个路径
* startX 和 startY 一起组成了起点的坐标
* endX 和 endY 一起组成了线段终点的坐标
*/
function draw(startX, startY, endX, endY) {
// 起点
ctx.moveTo(startX, startY);
// 终点
ctx.lineTo(endX, endY);
// 调用 stroke,即可看到绘制的线条
ctx.stroke();
}
// 监听鼠标按下,得到按下时鼠标在画布上的坐标
canvas.addEventListener("mousedown", ({ x, y }) => {
isDown = true;
// 按下时的点作为起点
lastX = x;
lastY = y;
// 创建一个新的路径
ctx.beginPath();
}, false);
// 监听鼠标移动
canvas.addEventListener("mousemove", ({ x, y }) => {
// 没有按下就不管
if (!isDown) return;
// 调用绘制方法
draw(lastX, lastY, x, y);
// 把当前移动时的坐标作为下一次的绘制路径的起点
lastX = x;
lastY = y;
}, false);
// 监听鼠标抬起
canvas.addEventListener("mouseup", () => {
isDown = false;
// 关闭路径
ctx.closePath();
}, false);
// 监听鼠标移出
canvas.addEventListener("mouseleave", () => {
// 移出canvas范围,也认为是鼠标抬起了,再移入需要重新按下鼠标,避免移出之后再抬起鼠标,重新进入画笔还能继续画的问题
isDown = false;
ctx.closePath();
}, false);
};上述代码片段已实现了最基础的签名绘制功能。
接下来,实现将 canvas 内容导出为图片的功能。
该功能实现较为简单,具体逻辑已在代码注释中说明。
// 使用canvas的toDataURL()方法,将画布内容转换为base64格式的图片数据:
let imgData = canvas.toDataURL('image/png');
// 创建下载链接
let link = document.createElement('a');
link.download = 'picture.png';
link.href = imgData;
// 触发点击
link.click();
// 移除元素
document.body.removeChild(link);最后,增加撤销(上一步)和重写(清空)功能。
梳理撤销与重写的实现思路。
要实现撤销笔画回到上一步,必须记录之前的绘制状态。具体做法是使用一个数组来存储每次鼠标移动产生的坐标点。
基于基础功能可知,画布上的签名由多条线条组成,而每条线条又由多个坐标点构成。因此,撤销操作的对象应是一条完整的线条(即一个笔画),而非单个坐标点。
为了区分哪些点属于同一个笔画,需要对点进行分组。规定从鼠标按下到鼠标抬起期间产生的所有坐标点归为一组,即一个笔画。在代码实现上,由于存在多个笔画,每个笔画又是一个点数组,因此使用二维数组来存储所有点的集合。
基于上述思路,对前文代码进行重构。
window.onload = function () {
// 默认鼠标是没有按下的
let isDown = false;
// // 记录上一次鼠标的位置
// let lastX = 0; // x轴
// let lastY = 0; // y轴
// 这次要用数组来记录
let points = []; // 这是一个笔画的点
let allPonits = []; // 这是所有笔画的点
// 获取canvas元素
const canvas = document.getElementById("canvas"); // 获取canvas的上下文
const ctx = canvas.getContext("2d"); // 定义线条的宽度,即画笔的粗细
ctx.lineWidth = 3; // 定义画笔的颜色
ctx.strokeStyle = "#000";
function draw(startX, startY, endX, endY) {
// 起点
ctx.moveTo(startX, startY);
// 终点
ctx.lineTo(endX, endY);
// 调用 stroke,即可看到绘制的线条
ctx.stroke();
}
// 监听鼠标按下,得到按下时鼠标在画布上的坐标
canvas.addEventListener("mousedown", ({ x, y }) => {
isDown = true;
// lastX = x;
// lastY = y;
// 保存当前坐标作为起点
points.push({ x, y });
// 创建一个新的路径
ctx.beginPath();
}, false);
// 监听鼠标移动
canvas.addEventListener("mousemove", ({ x, y }) => {
// 没有按下就不管
if (!isDown) return;
// 调用绘制方法
// draw(lastX, lastY, x, y);
// 把当前移动时的坐标作为下一次的绘制路径的起点
// lastX = x;
// lastY = y;
// 每次都取最后一个点,作为绘制的起点
const lastPoint = points.at(-1);
draw(lastPoint.x, lastPoint.y, x, y);
// 把当前的点保存起来,又作为下一次绘制的起点
points.push({ x, y });
}, false);
// 监听鼠标抬起
canvas.addEventListener("mouseup", (e) => {
isDown = false;
// 关闭路径
ctx.closePath();
// 鼠标抬起,说明当前这一笔就结束了,把这一笔的所有点的数组放到总的里面
allPonits.push(points);
// 清空这一笔画,为下一笔画做准备
points = [];
}, false);
// 监听鼠标移出
canvas.addEventListener("mouseleave", () => {
// 移出canvas范围,也认为是鼠标抬起了,再移入需要重新按下鼠标,避免移出之后再抬起鼠标,重新进入画笔还能继续画的问题
isDown = false;
// 关闭画笔
ctx.closePath();
// 如果是先抬起鼠标再移出,那么points里面为空,就不保存了
// 如果是先移出范围,移出时就保存,这样也不会触发上面的监听鼠标抬起事件,也不会push。
if (points.length) {
allPonits.push(points);
}
// 移出时也清空,因为无法判断是先抬起还是先移出的。
points = [];
}, false);
};在页面中添加两个按钮,分别用于触发“上一步”和“重写”操作。
<div>
<button id="prev">上一步</button>
<button id="reset">重写</button>
</div>const prev = document.getElementById("prev");
const reset = document.getElementById("reset");
// 清空画布
function resetPath() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
// 上一步
prev.addEventListener("click", (e) => {
// canvas本身不会记录用户的每一步操作
// 要回到上一步,只能一次性清空所有的
resetPath();
// 删除最后一个笔画
allPonits.pop();
// 遍历所有的笔画并重新绘制
// allPoints 是个二维数组
allPonits.forEach((ps) => {
ps.forEach((item, index) => {
// 下一个坐标点
let next = ps[index + 1];
if (next) {
// 有下一个点才执行,否则到最后一个会报错
// 开始重新绘制
ctx.beginPath();
draw(item.x, item.y, next.x, next.y);
ctx.closePath();
}
});
});
});
// 重写
reset.addEventListener("click", () => {
// 点击重写时清空画布,并清空所有的点
resetPath();
allPonits = [];
}, false);至此,我们完成了基于 canvas 的手写签名功能开发,并成功实现了撤销笔画、重写清空以及导出图片的功能。
以上为 JavaScript 使用 canvas 实现手写签名的完整教程,更多相关技术文章请持续关注本站。
JS监听滚动事件:回到顶部按钮的显示与隐藏逻辑
2026-11-05 16:19:37
JavaScript怎么读写LocalStorage与SessionStorage
2026-11-05 15:40:11
anime.js 与 GSAP 对比:轻量级项目该怎么选择动画库
2026-11-05 11:24:55
flv.js 与 hls.js 在直播场景下的选型对比
2026-11-05 08:18:52
JavaScript能做什么?前端开发中的作用与应用场景解析
2026-11-04 23:17:54
