学习与 前端实现电子签名
是通过原生canvas实现,我这篇是对其进行了改造,记录一下
效果图
组件 components/esign.vue
<template>
<div style="width: 100%;height: 100%;">
<div class="drawing-board">
<canvas
id="canvas"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
></canvas>
</div>
<!-- <div class="tool-bar">
<van-button @click="reset()" size="small" type="default">清除</van-button>
<van-button @click="save()" size="small" type="primary">确定</van-button>
</div> -->
</div>
</template>
<script>
const $ = (name) => document.querySelector(name);
// 配置内容
const config = {
width: 0, // 宽度
height: 0, // 高度
lineWidth: 5, // 线宽
strokeStyle: '#000000', // 线条颜色
lineCap: 'round', // 设置线条两端圆角
lineJoin: 'round', // 线条交汇处圆角
};
// 偏移量
const client = {
offsetX: 0,
offsetY: 0,
};
let canvas;
let ctx;
export default {
mounted() {
this.drawingBoardInit();
},
methods: {
drawingBoardInit() {
const {
width, height, left, top,
} = $('.drawing-board').getBoundingClientRect();
config.width = width;
config.height = height;
client.offsetX = left;
client.offsetY = top;
// canvas 实例
canvas = $('#canvas');
// 设置宽高
canvas.width = config.width;
canvas.height = config.height;
// 设置边框
// canvas.style.border = '1px solid #000';
// 创建上下文
ctx = canvas.getContext('2d');
// 设置填充背景色
ctx.fillStyle = 'transparent';
// 绘制填充矩形
ctx.fillRect(
0, // x 轴起始绘制位置
0, // y 轴起始绘制位置
config.width, // 宽度
config.height, // 高度
);
},
// 鼠标按下
touchStart(event) {
event.preventDefault();
// 获取偏移量及坐标
const { clientX, clientY } = event.changedTouches[0];
// 清除以上一次 beginPath 之后的所有路径,进行绘制
ctx.beginPath();
// 根据配置文件设置相应配置
ctx.lineWidth = config.lineWidth;
ctx.strokeStyle = config.strokeStyle;
ctx.lineCap = config.lineCap;
ctx.lineJoin = config.lineJoin;
// 设置画线起始点位(减去 左边、上方的偏移量很关键)
ctx.moveTo(clientX - client.offsetX, clientY - client.offsetY);
},
// 绘制
touchMove(event) {
// 获取当前坐标点位
const { clientX, clientY } = event.changedTouches[0];
// 根据坐标点位移动添加线条(减去 左边、上方的偏移量很关键)
ctx.lineTo(clientX - client.offsetX, clientY - client.offsetY);
// 绘制
ctx.stroke();
},
// 结束绘制
touchEnd() {
// 结束绘制
ctx.closePath();
// 移除鼠标移动或手势移动监听器
window.removeEventListener('mousemove', this.draw);
},
// 清除
reset() {
// 清空当前画布上的所有绘制内容
ctx.clearRect(0, 0, config.width, config.height);
},
// 将画布内容保存为图片
save() {
return new Promise((resolve, reject) => {
if (!this.isCanvasBlank(canvas)) {
this.rotateBase64(canvas.toDataURL('image/png')).then((img) => {
const imgBase64 = img;
// console.log(imgBase64, 'imgBase64-->>'); // base64编码
resolve(imgBase64);
});
} else {
const err = '请签名';
reject(err);
}
});
// 将canvas上的内容转成blob流
// canvas.toBlob((blob) => {
// console.log(blob, 'blob-->>'); // 文件二进制流
// // 获取当前时间,用来当做文件名
// const date = new Date().getTime();
// // 创建一个 a 标签
// const link = document.createElement('a');
// // 设置 a 标签的下载文件名
// link.download = `${date}.png`;
// // 设置 a 标签的跳转路径为 文件流地址
// link.href = URL.createObjectURL(blob);
// // 手动触发 a 标签的点击事件
// link.click();
// // 移除 a 标签
// link.remove();
// });
},
// 判断canvas对象是否空
isCanvasBlank(canvas) {
const blank = document.createElement('canvas'); // 系统获取一个空canvas对象
blank.width = config.width;
blank.height = config.height;
return canvas.toDataURL() === blank.toDataURL(); // 比较值相等则为空
},
// 将base64图片旋转90度以后上传
rotateBase64(imgBase64) {
return new Promise((resolve) => {
const imgView = new Image();
imgView.src = imgBase64;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const cutCoor = {
sx: 0,
sy: 0,
ex: 0,
ey: 0,
};
// 裁剪坐标
imgView.onload = () => {
const imgW = imgView.width;
const imgH = imgView.height;
const size = imgH;
// 常量大小 = imgH;
canvas.width = size * 2;
canvas.height = size * 2;
cutCoor.sx = size;
cutCoor.sy = size - imgW;
cutCoor.ex = size + imgH;
cutCoor.ey = size + imgW;
context.translate(size, size);
context.rotate((Math.PI / 2) * 3);
context.drawImage(imgView, 0, 0);
const imgData = context.getImageData(cutCoor.sx, cutCoor.sy, cutCoor.ex, cutCoor.ey);
canvas.width = imgH;
canvas.height = imgW;
context.putImageData(imgData, 0, 0);
resolve(canvas.toDataURL('image/png'));
};
});
},
},
};
</script>
<style scoped lang="stylus">
.drawing-board {
width: 100%;
height: calc(100%);
/* border-bottom: 1px solid #ccc; */
box-sizing: border-box;
}
.tool-bar {
width: 100%;
height: 40px;
display: flex;
justify-content: flex-end;
align-items: center;
position: absolute;
top: 46%;
left: -4rem;
transform: rotate(90deg);
.van-button{
flex: 1
}
}
</style>
使用组件
import vueEsign from '@/components/esign';
components: {
vueEsign
}
<div class="esign">
<vue-esign ref="esign" />
</div>
// 因为canvas 根据外部宽高设置的
.esign {
height 100vh
}
清除
this.$refs.esign.reset();
获取图片base64
this.$refs.esign
.save()
.then((res) => {
this.resultImg = res;
})
.catch((err) => {
// 画布没有签字时会执行这里
this.$dialog.alert({
message: err,
});
});