随着时代的发展,以前的手写签名,逐渐衍生出了电子签名,比如钉钉等系统中等电子签名,电子签名和纸质手写签名一样具有法律效应。
作为前端等我们如何实现电子签名呢?其实在html5中已经出现了一个重要等辅助标签——canvas
canvas
是一个可以在上面通过JavaScript
画图等标签,通过提供等context(上下文)
以及API
进行绘制,在这个过程中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
等其他属性,设置线的交汇处是否有圆角边等等这些操作可以自己去尝试选择。
先来看比较简单的清空功能
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
其实已经提供了两种方法来进行图片导出
在日常中我们更推荐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
,此时我们就也支持移动端了,好了一个相对完整的电子签功能已经实现了
环境uni-app
创建canvas节点
<canvas class="form-content__canvas" canvas-id="canvas_sign" @touchstart="touchstart"
@touchmove="touchmove" @touchend="touchend" disable-scroll="true">canvas>
触摸开始(touchstart)
touchstart(e) {
if (!this.isInit) {
this.isInit = true
this.autographClick(1);
}
let startX = e.changedTouches[0].x
let startY = e.changedTouches[0].y
let startPoint = {
X: startX,
Y: startY
}
this.points.push(startPoint)
//每次触摸开始,开启新的路径
this.canvasCtx.beginPath()
}
获取最开始的坐标,x和y,然后将他们存起来,注意每次触摸后都应该调用 beginPath()。
触摸移动(touchmove)
touchmove(e) {
let moveX = e.changedTouches[0].x
let moveY = e.changedTouches[0].y
let movePoint = {
X: moveX,
Y: moveY,
T: new Date().getTime(),
W: (MAX_LINE_WIDTH + MIN_LINE_WIDTH) / 2
}
this.points.push(movePoint) //存点
if (lastPoint) {
// console.log(lastPoint.T, movePoint.T)
movePoint.W = this.calcLineWidth(movePoint); // 重新赋值宽度,覆盖默认值
this.canvasCtx.beginPath();
this.canvasCtx.strokeStyle = '#000';
this.canvasCtx.lineCap = 'round';
this.canvasCtx.lineJoin = 'round';
this.canvasCtx.lineWidth = movePoint.W;
this.canvasCtx.moveTo(lastPoint.X, lastPoint.Y);
this.canvasCtx.lineTo(movePoint.X, movePoint.Y);
this.canvasCtx.stroke();
}
lastPoint = movePoint; // 结束前保存当前点为上一点
let len = this.points.length
if (len >= 2) {
this.draw() //绘制路径
}
}
获取移动的坐标,将坐标、时间和画笔宽度进行存点。
笔锋效果(calcLineWidth)
const MAX_V = 1; // 最大书写速度
const MIN_V = 0; // 最小书写速度
const MAX_LINE_WIDTH = 16; // 最大笔画宽度
const MIN_LINE_WIDTH = 4; // 最小笔画宽度
const MAX_LINE_DIFF = .03; // 两点之间笔画宽度最大差异
let context = null; // canvas上下文
let lastPoint = null; // 包含上一点笔画信息的对象
calcLineWidth(movePoint) {
let consuming = movePoint.T - lastPoint.T; // 两点之间耗时
if (!consuming) return lastPoint.W; // 如果当前点用时为0,返回上点的宽度。
// 当前点的最大宽度
let maxWidth = Math.min(MAX_LINE_WIDTH, lastPoint.W * (1 + MAX_LINE_DIFF));
// 当前点的最小宽度,变细时速度快所以宽度变化要稍快
let minWidth = Math.max(MIN_LINE_WIDTH, lastPoint.W * (1 - MAX_LINE_DIFF * 3));
// 两点之间距离
let distance = Math.sqrt(Math.pow(movePoint.X - lastPoint.X, 2) + Math.pow(movePoint.Y - lastPoint.Y, 2));
/*当前点速度*/
let speed = Math.max(Math.min(distance / consuming, MAX_V), MIN_V);
/* 当前点宽度 */
let lineWidth = Math.max(Math.min(MAX_LINE_WIDTH * (1 - speed / MAX_V), maxWidth), minWidth);
return lineWidth;
}
在绘制的过程,通过两点之间的距离、速度计算出宽度,再进行绘制,也就是笔锋的效果。可以自己调节初始值,设置成自己最想要的效果。
绘制笔迹(draw)
draw() {
let point1 = this.points[0]
let point2 = this.points[1]
this.points.shift()
this.canvasCtx.moveTo(point1.X, point1.Y)
this.canvasCtx.lineTo(point2.X, point2.Y)
this.canvasCtx.stroke()
this.canvasCtx.draw(true)
this.hasSign = true
}
触摸结束(touchend)
touchend() {
this.points = []
this.canvasCtx.draw(true)
lastPoint = null;
}
最后别忘记再每次结束时,要进行清空。
文章参考
https://blog.csdn.net/Robergean/article/details/128863208
https://juejin.cn/post/7146598432285655054