移动端用canvas手写签名,实现思路不难,主要就是绘制笔迹,生成图片上传。
笔迹的绘制,我们需要考虑的点就是线条的粗细,锯齿问题。如果对PS熟悉,我们就知道,圆角放大到像素点后,就会看到还是有很多的小锯齿,只是这些小锯齿有的透明,有的半透明。缩小后看起来就是比较圆润流畅的。对此,我们可以通过对canvas线条添加少量的阴影来模拟处理边缘的半透明锯齿,以此达到笔迹路劲看起来圆润流畅无锯齿。
线条端点默认是平直的边缘,会比较生硬,可以给线条添加圆形线帽使其圆滑,lineCap:属性设置或返回线条末端线帽的样式。
折线处线条也是生硬的,设置线条交叉拐角为圆形,使其圆润,lineJoin:设置或返回两条线相交时,所创建的拐角类型。
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.shadowBlur = 1;
this.ctx.shadowColor = '#000';
签名内容可大可小,生成的图片可能在签名周围有很多空白,为了在合同或协议等需要展示签名的地方更好看的展示签名,我们还需要把签名周围的空白去掉。通过遍历查找图片有颜色的区域划定处内容范围。再把这个范围内的图像提取出来。实现方法是把有签名内容的矩形区域绘制到一个新的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 tempCanvas2 = document.createElement('canvas');
tempCanvas2.width = tempCanvas.height;
tempCanvas2.height = tempCanvas.width;
let tempCtx2 = tempCanvas2.getContext("2d");
tempCtx2.rotate(-90*Math.PI/180);
tempCtx2.translate(-tempCanvas2.height, 0);
tempCtx2.drawImage(tempCanvas, 0, 0);
let imgBolb = this.getImageBolb(tempCanvas2.toDataURL('image/png'));
let imgDataUrl = tempCanvas2.toDataURL('image/png');
// 清除临时对象
tempCanvas = null;
tempCanvas2 = null;
tempCtx = null;
tempCtx2 = null;
return {imgBolb, imgDataUrl};
}
签名绘画裁剪完成后,我们就可以把新的canvas转成图片进行上传了,上传的图片需要转成二进制。
getImageBolb(imgDataUrl){
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 Blob([u8arr], { type: mimeType });
}
最终完整代码示例:
<style>
body, canvas{margin: 0; padding: 0;}
.box{
position: absolute;
overflow: hidden;
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
.box .header{
width: 100%;
height: .8rem;
line-height: .8rem;
text-align: center;
background: #000;
color: #FFF;
}
#signature{
flex: 1;
width: 100%;
border: 1px dashed #ccc;
margin: .1rem;
box-sizing: border-box;
}
.btns{
width: 100%;
height: 1rem;
display: flex;
justify-content: space-around;
align-items: center;
}
</style>
<div class="box">
<div class="header">签名</div>
<div id="signature"></div>
<div class="btns">
<button class="clearBtn">清除</button>
<button class="ensureBtn">确定</button>
</div>
</div>
<script>
class Signature{
constructor(selector){
this.$canvasBox = document.querySelector(selector);
this.width = this.$canvasBox.clientWidth;
this.height = this.$canvasBox.clientHeight;
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.$canvasBox.appendChild(this.canvas);
this.left = this.canvas.offsetLeft;
this.top = this.canvas.offsetTop;
this.ctx = this.canvas.getContext('2d');
this.bindEvent();
}
bindEvent(){
this.canvas.addEventListener('touchstart', this.pathStart.bind(this));
this.canvas.addEventListener('touchmove', this.pathMove.bind(this));
this.canvas.addEventListener('touchend', this.pathEnd.bind(this));
}
offEvent(){
this.canvas.addEventListener('touchstart', this.pathStart.bind(this));
this.canvas.removeEventListener('touchmove', this.pathMove.bind(this));
this.canvas.removeEventListener('touchend', this.pathEnd.bind(this));
}
pathStart(e){
let touch = e.touches[0];
let x = touch.clientX - this.left,
y = touch.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){
let touch = e.touches[0];
let x = touch.clientX - this.left,
y = touch.clientY - this.top;
this.ctx.lineTo(x, y);
this.ctx.stroke();
}
pathEnd(e){
this.ctx.closePath();
}
clean(){
this.ctx.clearRect(0,0,this.width,this.height);
}
getImageBolb(imgDataUrl){
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 Blob([u8arr], { 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 tempCanvas2 = document.createElement('canvas');
tempCanvas2.width = tempCanvas.height;
tempCanvas2.height = tempCanvas.width;
let tempCtx2 = tempCanvas2.getContext("2d");
tempCtx2.rotate(-90*Math.PI/180);
tempCtx2.translate(-tempCanvas2.height, 0);
tempCtx2.drawImage(tempCanvas, 0, 0);
let imgBolb = this.getImageBolb(tempCanvas2.toDataURL('image/png'));
let imgDataUrl = tempCanvas2.toDataURL('image/png');
// 清除临时对象
tempCanvas = null;
tempCanvas2 = null;
tempCtx = null;
tempCtx2 = null;
return {imgBolb, imgDataUrl};
}
destroy(){
this.ctx = null;
this.canvas = null;
this.offEvent();
}
}
const signatureImg = new Signature('#signature');
const $clearBtn = document.querySelector('.clearBtn');
const $ensureBtn = document.querySelector('.ensureBtn');
$clearBtn.addEventListener('click', function(){
signatureImg.clean();
});
$ensureBtn.addEventListener('click', function(){
let imgInfo = signatureImg.getSignatureImage();
console.log(imgInfo);
let img = document.createElement('img');
img.src = imgInfo.imgDataUrl;
img.style.marginTop = '2rem';
document.body.appendChild(img);
});
</script>
转载请注明带链来源:春语精椿