最近遇到一个h5手写签名的需求,按理说这种功能网上随便一搜一大把现成的源码和组件,但是像这种比较经典又很简单的功能,还是要弄清楚到底怎么实现的。
先实现基本需求(能签名即可)
整理思路
-
1. 准备一个canvas画布,得到context对象
-
2. 指定画笔的样式
-
3. 监听鼠标 / 手指的移动,得到每一次移动在画布上的坐标点,记录下来
-
4. 将这些点绘制到画布上形成线条
<canvas width="600" height="400" id="canvas" style="background-color: #ddd;"></canvas>
为了方便调试,我们本次仅演示pc端的操作。移动端思路是一样的,只不过监听的API不同。
常见的操作方式是:当鼠标左键按下的时候在画布上移动鼠标,就可以绘制。没有按下鼠标时,不管它。
注释很重要,我尽量写得很详细
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",
(e) => {
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);
撤销和重写功能
整理思路
-
1. 要实现撤销笔画回到上一步,就要知道上一步画了什么,就是要记录下来,我们可以用一个数组,把每次鼠标移动时得到的坐标放进去。
-
2. 通过基础功能我们知道了,画布上签名,是由多个线条组成的,而线条是由很多个点组成的。那我们撤销的时候,是撤销一条线,即一个笔画,而不是一个点。
-
3. 那么,怎么知道哪些点是属于一个笔画的呢,就是要给这些点分组,一个笔画为一组。我们规定,从鼠标按下到鼠标抬起,这之间移动时产生的所有点为一组,即一个笔画。用代码表示,就是有多个数组,所以我们定一个二维数组来保存所有的点。
// 改写一下前面的代码
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
);
};
在页面上加两个按钮
<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手写签名,并且实现了撤销和重写,以及导出为图片的功能。