在PC端用canvas绘制签名,主要就是用鼠标绘制笔迹,生成图片上传,用到mouse事件和File对象。
笔迹的绘制,我们需要考虑的点就是线条的粗细,锯齿问题。如果对PS熟悉,我们就知道,圆角放大到像素点后,就会看到还是有很多的小锯齿,只是这些小锯齿有的透明,有的半透明。缩小后看起来就是比较圆润流畅的。对此,我们可以通过对canvas线条添加少量的阴影来模拟处理边缘的半透明锯齿,以此达到笔迹路劲看起来圆润流畅无锯齿。
线条端点默认是平直的边缘,会比较生硬,可以给线条添加圆形线帽使其圆滑,lineCap:属性设置或返回线条末端线帽的样式。
折线处线条也是生硬的,设置线条交叉拐角为圆形,使其圆润,lineJoin:设置或返回两条线相交时,所创建的拐角类型。
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.shadowBlur = 1;
this.ctx.shadowColor = '#000';
绘制签名的区域在屏幕上可能会变动,比如缩放浏览器大小,绘制的笔记要在鼠标移动位置上,就要先确定要画布和窗口的距离,鼠标和窗口的距离。
通常签名区的容器在浏览器窗口是全部可见,没有滚动条的。因此可以使用getBoundingClientRect方法获取响应的距离数据。
鼠标的位置可以通过鼠标事件,在事件对象里获取。
getBoundingClientRect()
用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。
getCanvasPosition(){
let _position = this.$canvasBox.getBoundingClientRect();
this.left = _position.left;
this.top = _position.top;
}
签名内容可大可小,生成的图片可能在签名周围有很多空白,为了在合同或协议等需要展示签名的地方更好看的展示签名,我们还需要把签名周围的空白去掉。通过遍历查找图片有颜色的区域划定处内容范围。再把这个范围内的图像提取出来。实现方法是把有签名内容的矩形区域绘制到一个新的canvas上面,新的canvas宽高和签名内容矩形区域宽高一致。然后把新的canvas导出图片去上传使用。
getSignatureImage(){
let imgData = this.ctx.getImageData(0,0,this.canvas.width, this.canvas.height);
let data = imgData.data;
let lOffset = this.canvas.width, rOffset = 0,tOffset = this.canvas.height, bOffset = 0;
for (let i = 0; i < this.canvas.width; i++) {
for (let j = 0; j < this.canvas.height; j++) {
let pos = (i + this.canvas.width * j) * 4
if (data[pos + 3] > 0) {
// 这个条件说第j行第i列的像素不是透明的
bOffset = Math.max(j, bOffset); // 找到不透明区域最底部的纵坐标
rOffset = Math.max(i, rOffset); // 找到不透明区域的最右端
tOffset = Math.min(j, tOffset); // 找到不透明区域的最上端
lOffset = Math.min(i, lOffset); // 找到不透明区域的最左端
}
}
}
lOffset++;
rOffset++;
tOffset++;
bOffset++;
// 未签名,画布上无内容
if(lOffset > this.width || tOffset > this.height){
return null;
}
// 裁剪签名内容周围空白区域
let tempCanvas = document.createElement('canvas');
tempCanvas.height = bOffset - tOffset;
tempCanvas.width = rOffset - lOffset;
let tempCtx = tempCanvas.getContext("2d");
let newImgData = this.ctx.getImageData(lOffset, tOffset, (rOffset - lOffset), (bOffset - tOffset));
tempCtx.putImageData(newImgData, 0, 0 );
let imgFile = this.getImageFile(tempCanvas2.toDataURL('image/png'));
let imgDataUrl = tempCanvas2.toDataURL('image/png');
// 清除临时对象
tempCanvas = null;
tempCtx = null;
return {imgFile, 'signature.png', imgDataUrl};
}
签名绘画裁剪完成后,我们就可以把新的canvas转成图片进行上传了,上传的图片需要转成文件。
getImageFile(imgDataUrl, filename){
const arr = imgDataUrl.split(',');
const mimeType = arr[0].match(/:(.*?);/)[1];
const bStr = atob(arr[1]);
let n = bStr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bStr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mimeType });
}
最终完整的签名类如下:
<script>
export default class Signature{
constructor(selector){
this.$canvasBox = document.querySelector(selector);
this.width = this.$canvasBox.clientWidth;
this.height = this.$canvasBox.clientHeight;
this.drawing = false;
this.left = 0;
this.top = 0;
this.init();
}
init(){
this.canvas = document.createElement('canvas');
this.canvas.width = this.width;
this.canvas.height = this.height;
this.canvas.style.backgroundColor = 'rgba(255,255,255,0)'; // 画布透明,可以在画布下面展示如“签名区”文案。
this.$canvasBox.appendChild(this.canvas);
this.getCanvasPosition();
this.ctx = this.canvas.getContext('2d');
this.bindEvent();
}
getCanvasPosition(){
let _position = this.$canvasBox.getBoundingClientRect();
this.left = _position.left;
this.top = _position.top;
}
bindEvent(){
window.addEventListener('resize', ()=>{
this.getCanvasPosition();
});
this.canvas.addEventListener('mousedown', this.pathStart.bind(this));
this.canvas.addEventListener('mousemove', this.pathMove.bind(this));
this.canvas.addEventListener('mouseup', this.pathEnd.bind(this));
}
offEvent(){
this.canvas.removeEventListener('mousedown', this.pathStart.bind(this));
this.canvas.removeEventListener('mousemove', this.pathMove.bind(this));
this.canvas.removeEventListener('mouseup', this.pathEnd.bind(this));
}
pathStart(e){
this.isDrawing = true;
let x = e.clientX - this.left,
y = e.clientY - this.top;
this.ctx.lineWidth = '3';
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.shadowBlur = 1;
this.ctx.shadowColor = '#000';
this.ctx.beginPath();
this.ctx.moveTo(x, y);
}
pathMove(e){
if(!this.isDrawing){
return;
}
let x = e.clientX - this.left,
y = e.clientY - this.top;
this.ctx.lineTo(x, y);
this.ctx.stroke();
}
pathEnd(e){
this.ctx.closePath();
this.isDrawing = false;
}
clean(){
this.ctx.clearRect(0,0,this.width,this.height);
}
_uuid(){
var d = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
getImageFile(imgDataUrl, filename){
const arr = imgDataUrl.split(',');
const mimeType = arr[0].match(/:(.*?);/)[1];
const bStr = atob(arr[1]);
let n = bStr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bStr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mimeType });
}
getSignatureImage(){
let imgData = this.ctx.getImageData(0,0,this.canvas.width, this.canvas.height);
let data = imgData.data;
let lOffset = this.canvas.width, rOffset = 0,tOffset = this.canvas.height, bOffset = 0;
for (let i = 0; i < this.canvas.width; i++) {
for (let j = 0; j < this.canvas.height; j++) {
let pos = (i + this.canvas.width * j) * 4;
if (data[pos + 3] > 0) {
// 这个条件说第j行第i列的像素不是透明的
bOffset = Math.max(j, bOffset); // 找到不透明区域最底部的纵坐标
rOffset = Math.max(i, rOffset); // 找到不透明区域的最右端
tOffset = Math.min(j, tOffset); // 找到不透明区域的最上端
lOffset = Math.min(i, lOffset); // 找到不透明区域的最左端
}
}
}
lOffset++;
rOffset++;
tOffset++;
bOffset++;
// 未签名,画布上无内容
if(lOffset > this.width || tOffset > this.height){
return null;
}
// 裁剪签名内容周围空白区域
let tempCanvas = document.createElement('canvas');
tempCanvas.height = bOffset - tOffset;
tempCanvas.width = rOffset - lOffset;
let tempCtx = tempCanvas.getContext('2d');
let newImgData = this.ctx.getImageData(lOffset, tOffset, (rOffset - lOffset), (bOffset - tOffset));
tempCtx.putImageData(newImgData, 0, 0 );
let imgFile = this.getImageFile(tempCanvas.toDataURL('image/png'), this._uuid()+'.png');
let imgDataUrl = tempCanvas.toDataURL('image/png');
// 清除临时对象
tempCanvas = null;
tempCtx = null;
return {imgFile, imgDataUrl};
}
destroy(){
this.offEvent();
this.$canvasBox.removeChild(this.canvas);
this.ctx = null;
this.canvas = null;
}
}
</script>