签名板介绍
在最近的一个项目中,最后的一个功能是实现一个签名板供客户签名使用。这需要用到canvas来实现。
我将这个功能分成实现和优化两个阶段。首先来看实现
实现
对于一个canvas签名板,首先我们要完成最最最基本的功能,签名!
步骤为下:
1、提供画布:
通过canvas的API,很容易定义出一块画布。
2、定义画笔
实际情况上是没有画笔这个东西的,所以需要自己来假定一个画笔。
画笔具有三个状态,准备画,画,画完了。用一个flag和三个方法来表示这一过程:
mousePress:false // 这个flag表示是否开始画了,准备画的时候这个标志位置为true,画完置为false。
beginDraw (e) { ... } // 对应准备画的方法
drawing (e) {...} // 对应画的方法
endDraw (e) {...} // 对应画完的方法 。。。 这三个方法的实现下面会具体讲,这里假定已经实现了。
3、实现签名
现在距离实现签名就差一步,那就是记录下鼠标每次经过的位置并用ctx.stroke()方法将它画出来即可。
这里需要知道:画过的路径不会变,将要画的路径不知道,正在画的地方能获取。所以定义一个叫做last的对象来记录上一次鼠标画过的点的位置并把它画到现在的位置,以此类推,就可以画出一条鼠标经过的线路了。现在来具体实现画画这一过程:
a、beginDraw
beginDraw(e) {
mousePress = true;
}, // 没什么好说的
b、endDraw
endDraw (e) {
e.preventDefault();
mousePress = false;
last = null;
} // 也没什么好说的,画完了标志位和记录对象都该还原了。
c、drawing
drawing (e) {
e.preventDefault();
if (!this.mousePress) {
return; // 这个虽然不会触发,但是好像需要这个逻辑也就写上去了
}
var xy = this.getCoordinate(e); // 这个方法是获取当前的鼠标的位置并将它赋值给一个叫做xy的对象
if (this.last != null) {
this.context.beginPath();
this.context.moveTo(this.last.x, this.last.y);
this.context.lineTo(xy.x, xy.y);
this.context.stroke();
}
// 开始移动,将坐标赋值给last。那么下次再移动就会通过上面的操作从上一个xy移动到当前的xy处
this.last = xy;
}
d、getCoordinate 获取当前鼠标位置
getCoordinate(e) {
var x, y;
x = e.offsetX + e.target.offsetLeft;
y = e.offsetY + e.target.offsetTo; // 获取当前x,y坐标
return {
x,y
} //es6语法多简洁
},
4、事件绑定
接下来只需要在canvas上绑定事件去对应准备画,画和结束画三个方法就可以了。
canvas.onmousedown = beginDraw; // 鼠标按下事件
canvas.onmouseup = endDraw; // 鼠标松开的事件
canvas.onmousemove = drawing; // 鼠标移动事件
好了,如果你稍微了解一些canvas的基础,补全这些代码,现在你的签名板就实现了。
优化
签名板,在电脑上签名,用鼠标签名,怎么说都显得怪异。
优化1、移动端的签名:
首先,把移动端的事件绑定到对应的三个方法:
canvas.addEventListener('touchstart',beginDraw,false)
canvas.addEventListener('touchmove',drawing,false)
canvas.addEventListener('touchend',endDraw,false)
然后,在移动端试了试,不行。那是因为在getCoordinate 方法中获取的当前位置有问题,在移动端,要用另一种方法来获取。为了区分移动端和PC,需要用一个控制语句来区分它们。如下
getCoordinate(e) {
var x, y;
if (this.isTouch(e)) { // 判断当前事件是移动端事件还是PC端事件
x = e.touches[0].pageX;
y = e.touches[0].pageY;
}
else {
x = e.offsetX + e.target.offsetLeft;
y = e.offsetY + e.target.offsetTop;
}
return {
x,y
}
},
isTouch(e) { // 判断当前事件是移动端事件还是PC端事件
var type = e.type;
if (type.indexOf('touch') >= 0) {
return true;
} else {
return false;
}
},
OK,现在签名板以及可以兼容PC端和移动端两个部分了。
优化2、个性化签名
当然,这个名还是要自己签的,不过可以自己设定签名的线条的粗细,颜色等。这些样式设定放在beginDraw这个方法处即可。具体实现就不说了。
优化3、用Vue来实现。
用Vue的话,更方便地控制这个项目中需要用的变量等。具体改变就是把上述需要定义的变量放在vue的data中,方法房子啊methods中。绑定事件在canvas处,如图:
这样也是非常的方便。
优化4、移动端横竖屏画布大小调整
在PC端的话,设定固定大小的画布即可。但对于移动端来说,画布的宽度需要撑满屏幕才勉强可以做到签名。这时候,需要实现将手机变成横屏时,监听手机的变化,改变画布的大小。这里,初始化画布宽度为手机屏幕的宽度,高度为width*380/750。
这时候,需要用到window.onorientationchange来监听横竖屏的转变和window.orientation来获取横竖屏转变的参数,如图所示:
0表示正常,90和-90表示横屏,180就是把手机倒过来。然后把orientationChange方法下面开始方案。
方案1、获取手机屏幕大小,直接进行改变(宽变高,高变宽)。
这是我最初的实现方案,这种直接改变画布宽和高是销毁了原来的画布重新生成了一张画布。确实达到了横屏签名的效果,但这却存在很大缺陷,那就是横竖屏转变重置画布会消除画布原有的内容。在实际场景中,有些客户如果想要横屏签字,竖屏再查看一下自己写的什么样子,就不满足于客户需求了。
方案2、由于横竖屏转变,画布必然会被重置,所以就不需要考虑如何让原有的签名内容不变保留过来了。解决方案就是在每次横竖屏转变的时候,获取原有签名板上内容数据,在横竖屏转变之后,将数据放入到新生成的画布中。这里,我将画布重置和画布内容导入封装到了一个叫做resize的方法中,代码如下:
/**
* 重置画布操作
* @param {*} w 画布宽
* @param {*} h 画布高
* @param {*} flag 横竖屏标志
*/
resize(w, h, flag) {
var that = this;
var nc = document.createElement("canvas"); // nc用来保留原有画布数据
nc.width = that.canvas.width;
nc.height = that.canvas.height;
nc.getContext("2d").drawImage(that.canvas, 0, 0); // 将原有画布数据放到同样大小的nc画布中,就像是copy了一份
this.canvas.width = w;
this.canvas.height = h; //画布大小重置
// 横转竖,原有数据放入到新的画布中。等比放置签名数据。
if (flag) {
// crossWidth是我设置的变量,表示横屏情况下,屏幕的宽度,verticalWidth表示竖屏情况下屏幕的宽度。
this.context.drawImage(nc, 0, 0, this.crossWidth, this.crossHeight, 0, 0, w, h);
}
// 竖转横
else {
this.context.drawImage(nc, 0, 0, this.verticalWidth, this.verticalHeight, 0, 0, w, h);
}
},
优化5、高清屏适配。
通常对于canvas来说,我们会在html中设置
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
这样一串东西来规定这个页面不能缩放,缩放比为1,这样我们在开发canvas的时候就不会因为不同手机的dpr不同导致画布呈现的有大有小,很不合理。当然,设置这样的一个约定不但不会影响签名板的视觉呈现(毕竟名字线条单一没那么复杂),也会很方便整个流程的开发。可是还是想让IOS的高清屏可以显示的更加清晰。
方案1、
由于项目采用的是手淘的flexible布局,这里,我们把上面的那个meta标签注释掉,如果你画布的宽度是这样定义的:
this.verticalWidth = screen.width。// 因为我会将这个verticalWidth赋值给canvas.width,见resize()方法。
如果你的是安卓机,那么画布没有变化,因为flexible默认设置的安卓dpr = 1,也就是initial-scale=1/dpr=1,和上面meta标签的配置是一样的。所以不会有变化。
如果你的是IPhone,那么你会发现你的画布大小变小了。这是因为initial-scale不再是1了,而是1/dpr。这时候,我们只需要在设置屏幕宽度的时候再乘上当前iPhone手机的dpr即可还原画布大小。
this.verticalWidth = screen.width*dpr // dpr需要自己获取
下面晒图,采用高清的IPhone的3像素的签名和不采用高清适配的签名对比
好吧,在签名这个功能处高清是真的没什么优势。。。。但是可能在其他的canvas功能中,这种高清适配会让canvas内容更加清晰。
对于安卓来说,暂时的想法就是将dpr为整数的安卓机在flexible中设置为其本身的dpr(dpr>=3 设置为3),这样就也可以让一大部分dpr比较正常的安卓机也可以像IPhone一样使用这种高清。
优化6、兼容性问题
在网上拜读了一位大佬对于orientation事件兼容性处理的方法后 文章,在签名板上也加入了orientation兼容性处理。
由于部分低端手机不支持orientation事件,所以要让他们也可以横屏签名,那么就要做一些改动。
解决方案:
保留原有方法不变,加上判断语句,如下代码:
var isOrientation = ('orientation' in window && 'onorientationchange' in window);
if (isOrientation) {
// 注册横竖屏事件,方案1;
window.onorientationchange = this.orientationChange;
this.orientationChange();
}
对于不支持orientation事件的机型,做resize事件处理(为了防止与我自己写的resize方法冲突,所以我把自己在前面写的resize方法修改成了resizeCanvas):
else {
// 使用 resize 来做监听机制。方案2:
window.addEventListener('resize',function(e){
var orientation=(window.innerWidth > window.innerHeight)? "landscape":"portrait"; // 判断横竖屏
if(orientation === 'portrait'){
// 竖屏 do something ……
that.screenCtrl = true;
setTimeout(function(){
that.height = that.getWarnHeight();
},500)
that.resizeCanvas(that.verticalWidth, that.verticalHeight, true);
return;
}
else {
// 横屏 do something else ……
that.screenCtrl = false;
setTimeout(function(){
that.height = that.getBtnAreaHeight();
that.crossHeight = window.innerHeight - that.height;
that.resizeCanvas(that.crossWidth, that.crossHeight, false);
},500)
that.resizeCanvas(that.crossWidth, that.crossHeight, false);
}
},false)
}
在加上上面的兼容处理后,基本所有的机型都可以使用了。
这一块会不定时更新如何优化,我还在学习怎样使canvas内容更加清晰。
额外补充
从上图可以看出,canvas画布左上角相对于页面而言不一定是(0,0)的位置。那么如果在drawing方法中不做些改变的话,在签名的时候笔迹和画出来的线条位置会不一样,这时候就需要计算出canvas左上角相对于页面左上角直接的差值,并改变drawing方法内的代码。补充代码,获取提示区域高度:
getWarnHeight() {
var warn = document.getElementById('warn');
var height = warn.offsetHeight;
warn = null;
return height;
},
总结
签名板 JS代码地址