uni-app电子签名功能

前言

随着时代的发展,以前的手写签名,逐渐衍生出了电子签名,比如钉钉等系统中等电子签名,电子签名和纸质手写签名一样具有法律效应。

作为前端等我们如何实现电子签名呢?其实在html5中已经出现了一个重要等辅助标签——canvas

说说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设置绘制线的颜色

但是我们看到了新增了lastXlastY,这是因为我们抽离出来单独方法了,每次绘制需要先moveTo移动到新的点去才能继续绘制,稍微有点不同,然后我们在mousemove的过程中传坐标过来即可,当然如果还想做其他的美化,比如lineJoinlineCap等其他属性,设置线的交汇处是否有圆角边等等这些操作可以自己去尝试选择。

实现书写撤销,清空功能

先来看比较简单的清空功能
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其实已经提供了两种方法来进行图片导出

  1. toDataURL,这个方法是同步的,转为base64,然后我们就可以导出了
  2. 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,此时我们就也支持移动端了,好了一个相对完整的电子签功能已经实现了

完整代码

环境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
}

  1. 为保证笔迹实时显示,必须在移动的同时绘制笔迹;
  2. 为保证笔迹连续,每次从路径集合中区两个点作为起点(moveTo)和终点(lineTo);
  3. 将上一次的终点作为下一次绘制的起点(即清除第一个点)。

触摸结束(touchend)

touchend() {
	this.points = []
	this.canvasCtx.draw(true)
	lastPoint = null;
}

最后别忘记再每次结束时,要进行清空。

文章参考
https://blog.csdn.net/Robergean/article/details/128863208
https://juejin.cn/post/7146598432285655054

你可能感兴趣的:(uni-app,javascript,前端)