我们日常中应该都经常遇到各种电子合同,需要我们去手动签名,往往只需要手动签名,那么是如何实现的呢?你有了解过么,看似很复杂其实非常简单,初级版本我们甚至只需要不到二十行代码即可实现。当然,真实的场景我们需要考虑更多,那么今天的主角依然是canvas,让我们开始吧!
准备工作
首先我们看到这个需求想到的就是鼠标按下的时候,开始画线,移动的过程中持续画这条线即可,所以第一时间我们想到的就是鼠标按下移动事件,所以我们先用mouse事件实现以下,第一步当然是需要在body中创建这个canvas元素了。
<canvas id="cvs"></canvas>
然后获取到这个元素并对其挂载上按下移动抬起的事件
const cvs = document.getElementById('cvs')
cvs.addEventListener('mousedown', (e) => {})
cvs.addEventListener('mousemove', (e) => {})
cvs.addEventListener('mouseup', (e) => {})
很通用的场景,我们需要在鼠标按下后才开始执行,所以我们定义一个变量,用于记录鼠标是否按下,在down的时候打开,up抬起鼠标的时候关闭,如果不是按下状态,那么我们移动中什么也不做
const cvs = document.getElementById('cvs')
let isDownin = false
cvs.addEventListener('mousedown', (e) => {
isDownin = true
})
cvs.addEventListener('mousemove', (e) => {
if(!isDownin) return
})
cvs.addEventListener('mouseup', (e) => {
isDownin = false
})
好了,准备工作已经完成,我们开始绘制
开始绘制
要绘制的思路很简单,当我们按下鼠标的时候,开始画线,将坐标移动到当前点击点,在移动过程中就会产生非常多的点,将这些点连成线不就可以了么,我们首先需要用到moveTo将坐标移到我们鼠标点下的点,然后在移动过程中使用lineTo将这些点连成线,最后使用stroke绘制出来就行,是不是很简单,我们看看具体代码。
const cvs = document.getElementById('cvs')
const ctx = cvs.getContext('2d')
let isDownin = false
cvs.addEventListener('mousedown', (e) => {
isDownin = true
ctx.moveTo(e.pageX, e.pageY)
})
cvs.addEventListener('mousemove', (e) => {
if(!isDownin) return
ctx.lineTo(e.pageX, e.pageY)
ctx.stroke()
})
cvs.addEventListener('mouseup', (e) => {
isDownin = false
})
就十五行代码就完成了一个看似很牛的功能,是不是很简单了,明白思路了么,当然这个是非常基础的版本,我们先来看看效果:
效果出来了,好像还不错,但是我们似乎不能自定义更多的东西,例如我不想要白纸黑字,例如我想要我笔粗一点这该怎么办呢?
自定义样式
我们想要自己定义笔的颜色,想要自己定义笔的宽度,可以怎么做呢,我们直接抽离一个方法吧,后续需要什么颜色,什么宽度自己改好了
function drawLine(x,y){
ctx.beginPath();
ctx.lineWidth = 5;
ctx.strokeStyle = '#fff';
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
lastX = x;
lastY = y;
}
这里我们可以给lineWidth设置宽度代表了笔的宽度,给strokeStyle设置颜色,代表绘制线的时候的颜色,但是我们看到了新增了lastX和lastY,这是因为我们抽离出来单独方法了,每次绘制需要先moveTo移动到新的点去才能继续绘制,稍微有点不同,然后我们在mousemove的过程中传坐标过来即可,当然如果还想做其他的美化,比如lineJoin,lineCap等其他属性,设置线的交汇处是否有圆角边等等这些操作可以自己去尝试选择,好了我们已经可以自定义绘制一个我们喜欢的样式了,看看效果。
貌似有点东西了,但是如果你是产品,我相信你这时会有这有一个疑问,我名字这么复杂,手写本来就不好写,我要是写错了咋办,重头开始?还是让我能回到上一笔,作为一个有经验的程序员,我会读产品说:不好意思、这个需求做不了、你找别人吧!
但是看看钱包,当然还是选择原谅她了。
实现书写回退,清空功能
现在我们写到一半了,需要清空,那当然easy了,一个按钮,点击之后我们清空即可,canvas的清空还不是简简单单,从(0,0)左上角到最右边,进行clearRect即可实现,这不是轻轻松松么。
ctx.clearRect(0, 0, width, height)
那么如何实现回退呢,要想实现后退,那么用户每画完一笔我们得保留他此次的轨迹,下次要是回来我们需要把数据放回去,如何实现呢?
通过 getImageData() 复制画布上指定矩形的像素数据,然后通过 putImageData() 将图像数据放回画布
这样一看,我们创建一个cacheData用户记录记录,每当我们按下鼠标的时候说明上次已经结束了,我们通过getImageData拿到之前的所有数据,push到cacheData,那么用户点击回退的时候,我们只需要拿到最后一项即可,然后通过putImageData放回去即可,这不是轻轻松松又实现了么?
ctx.getImageData(0, 0, width, height);
ctx.putImageData(cacheData.pop(), 0, 0);
好了我们已经实现了回退清空功能,产品又发话了,用户签完字得截图保存作为凭证啊,不然怎么知道用户签字了呢,也对,那我们还得想办法把用户签的字保存为图片,如何实现呢?
保存签名为图片
作为canvas其实已经提供了两种方法来进行图片导出
- toDataURL,这个方法是同步的,转为base64,然后我们就可以导出了
- toBlob可以将其转为blob流文件,这个方法是异步的,
在日常中我们更推荐toBlob,有看到别人说过如果toBlob可以早点出现,就完全没必要有toDataURL这个方法,一方面第一个api容易拼写错误,另一方面,这是一个同步方法,如果过大,这个是会进行堵塞的,所以以后我们优先选择toBlob,即可,使用非常简单,转为blob流文件,在回调函数中拿到他,然后我们创建一个a标签,然后通过URL.createObjectURL可以获取当前文件的一个内存URL,然后就可以下载了。
cvs.toBlob((blob) => {
const a = document.createElement('a');
document.body.append(a);
a.download = `签名.png`;
a.href = URL.createObjectURL(blob);
a.click();
a.remove();
});
好了,我们即可以让用户自己下载,也可以传到服务端保存,那么我们这个生成图片的功能就此完成了。
看似万事俱备之前东风了,按照以往的惯例,我们可以一把梭,然后上线了,但是我相信,上线不久产品会再次找到你,询问为什么手机上用不了,此时你应该恍然大悟,我们的mouse事件只支持pc啊,所以我们此时应该兼容手机端了。
兼容手机端了
由于码上掘金并不支持移动端,我们就不写示例代码了,只讲解思路即可,因为也非常简单,我们知道mouse事件对应的移动端是touch事件,所以,在使用前,我们应该先判断环境,当判断环境是移动端的情况下,我们就使用mouse对应的touchstart/touchmove/touchend/touchcancel
等事件即可,同时获取X,Y坐标的位置相对也改变一下,比如移动端的X坐标就是e.changedTouches[0].clientX
,此时我们就也支持移动端了,好了一个相对完整的电子签功能已经实现了,上线,下面可以找产品要鸡腿了。
在线体验
完整的代码在码上掘金大家可以自行体验
https://code.juejin.cn/pen/7146123537219076104