# 电子签名
# 场景介绍
项目开发中,业务需求可能会要求用户在一些协议中签字,这时前端就要用到canvas
绘制图形,并将它生成图片,上传到服务器上。本篇主要用canvas
搭配html2Canvas
、JsPDF
两个工具库去实现它。
TIP
本篇实现环境:vue3.2
# 实现
# canvas绘制
# 绘制事件
gloablAlpha
不透明度lineWidth
线宽strokeStyle
绘制样式moveTo
、lineTo
起点与落点beginPath
、closePath
开始路径、关闭路径stroke
绘制图形
const writing = (beginX, beginY, stopX, stopY, ctx) => {
ctx.beginPath();
ctx.globalAlpha = 1;
ctx.lineWidth = 3;
ctx.strokeStyle = "red";
ctx.moveTo(beginX, beginY);
ctx.lineTo(stopX, stopY);
ctx.closePath();
ctx.stroke();
};
# 添加事件监听--触屏点击
WARNING
注意要禁止默认事件的触发,防止在点击时,触发手机本身自带的点击功能。
- 移动点的
clientX
-canvas
文本对象据窗口左侧的距离==
点所在的坐标
signDom.addEventListener("touchstart", (e) => {
e.preventDefault();
beginX.value = e.touches[0].clientX - signDom.offsetLeft;
beginY.value = e.touches[0].pageY - signDom.offsetTop;
});
# 添加事件监听--触屏滑动
WARNING
注意要禁止默认事件的触发,防止滑动时,连着屏幕一块儿滑动。
- 绘制函数执行后,要更新起点值。否则起点不变的情况下,所有的末点移动都会生成 一条射线
- 移动点的
clientX
-canvas
文本对象据窗口左侧的距离 == 点所在的坐标
signDom.addEventListener("touchmove", (e) => {
e.preventDefault();
e = e.touches[0];
let stopX = e.clientX - signDom.offsetLeft;
let stopY = e.pageY - signDom.offsetTop;
writing(beginX.value, beginY.value, stopX, stopY, ctx);
beginX.value = stopX;
beginY.value = stopY;
});
# 转换成图片,生成PDF
allowTaint
是否允许跨域let imgHeight = (595.28 / canvasWidth) * canvasHeight
根据A4
与canvas
宽度,等比例缩放canvas
高度。(此时往小缩)let pageHeight = (canvasWidth / 595.28) * 841.89
根据A4
与canvas
宽度,等比例缩放 页面高度。(此时往大放)- 如果 页面高度 大于 图片高度,则直接生成图片
- 如果 图片高度 大于 页面高度,则在生成图片的同时,增加空白页。且
top
顶部值向下移动一个A4
页面高度的距离。图片高度减少一个A4
页面高度的距离。
const generateCanvas = () => {
// 获取包裹住它的Dom
const dom = document.getElementById("signature-canvas");
html2Canvas(dom, {
allowTaint: true, // 允许源跨域
width: dom.offsetWidth, //设置获取到的canvas宽度
height: dom.offsetHeight, //设置获取到的canvas高度
x: 0, //页面在水平方向滚动的距离
y: 0, //页面在垂直方向滚动的距离
}).then((canvas) => {
console.log("canvas",canvas);
let canvasWidth = canvas.width;
let canvasHeight = canvas.height;
// canvas高度== 自身宽度与A4宽度比例* A4的高度 == 0.5 * 841 == 420
// 图片高度imgHeight == A4与canvas宽度比 * canvas自身高 2 * 102 == 298
let pageHeight = (canvasWidth / 595.28) * 841.89; // 一页A4 pdf能显示的canvas高度
let imgWidth = 595.28; // 设置图片宽度和A4纸宽度相等
let imgHeight = (595.28 / canvasWidth) * canvasHeight; //等比例换算成A4纸的高度
let totalHeight = imgHeight; // 需要打印的图片总高度,初始状态和图片高度相等
let pageData = canvas.toDataURL("image/png", 1.0);
let PDF = new JsPDF("p", "pt", "a4", true);
if (totalHeight < pageHeight) {
//
PDF.addImage(pageData, "JPEG", 0, 0, imgWidth, imgHeight); // 从顶部开始打印
} else {
let top = 0; // 打印初始区域
while (totalHeight > 0) {
PDF.addImage(pageData, "JPEG", 0, top, imgWidth, imgHeight); // 从图片顶部往下top位置开始打印
totalHeight -= pageHeight;
top -= 841.89;
if (totalHeight > 0) {
PDF.addPage(); // 加一页
}
}
}
PDF.save("test.pdf");
});
};
# 完整代码示例
<script setup>
import { ref, onMounted } from "vue";
import html2Canvas from "html2canvas";
import JsPDF from 'jspdf';
const signRef = ref();
const canvaspic = ref();
let beginX = ref();
let beginY = ref();
onMounted(() => {
startCanvas();
});
const writing = (beginX, beginY, stopX, stopY, ctx) => {
ctx.beginPath();
ctx.globalAlpha = 1;
ctx.lineWidth = 3;
ctx.strokeStyle = "red";
ctx.moveTo(beginX, beginY);
ctx.lineTo(stopX, stopY);
ctx.closePath();
ctx.stroke();
};
const startCanvas = () => {
const signDom = document.getElementById("signature-canvas");
const ctx = signDom.getContext("2d");
signDom.addEventListener("touchstart", (e) => {
e.preventDefault();
beginX.value = e.touches[0].clientX - signDom.offsetLeft;
beginY.value = e.touches[0].pageY - signDom.offsetTop;
});
signDom.addEventListener("touchmove", (e) => {
e.preventDefault();
e = e.touches[0];
let stopX = e.clientX - signDom.offsetLeft;
let stopY = e.pageY - signDom.offsetTop;
writing(beginX.value, beginY.value, stopX, stopY, ctx);
beginX.value = stopX;
beginY.value = stopY;
});
};
const generateCanvas = () => {
// 获取包裹住它的Dom
const dom = document.getElementById("signature-canvas");
html2Canvas(dom, {
allowTaint: true, // 允许被污染?----允许源跨域
width: dom.offsetWidth, //设置获取到的canvas宽度
height: dom.offsetHeight, //设置获取到的canvas高度
x: 0, //页面在水平方向滚动的距离
y: 0, //页面在垂直方向滚动的距离
}).then((canvas) => {
console.log("canvas",canvas);
let canvasWidth = canvas.width;
let canvasHeight = canvas.height;
// canvas高度== 自身宽度与A4宽度比例* A4的高度 == 0.5 * 841 == 420
// 图片高度imgHeight == A4与canvas宽度比 * canvas自身高 2 * 102 == 298
let pageHeight = (canvasWidth / 595.28) * 841.89; // 一页A4 pdf能显示的canvas高度
let imgWidth = 595.28; // 设置图片宽度和A4纸宽度相等
let imgHeight = (595.28 / canvasWidth) * canvasHeight; //等比例换算成A4纸的高度
let totalHeight = imgHeight; // 需要打印的图片总高度,初始状态和图片高度相等
let pageData = canvas.toDataURL("image/png", 1.0);
let PDF = new JsPDF("p", "pt", "a4", true);
if (totalHeight < pageHeight) {
//
PDF.addImage(pageData, "JPEG", 0, 0, imgWidth, imgHeight); // 从顶部开始打印
} else {
let top = 0; // 打印初始区域
while (totalHeight > 0) {
PDF.addImage(pageData, "JPEG", 0, top, imgWidth, imgHeight); // 从图片顶部往下top位置开始打印
totalHeight -= pageHeight;
top -= 841.89;
if (totalHeight > 0) {
PDF.addPage(); // 加一页
}
}
}
PDF.save("test.pdf");
});
};
</script>
<template>
<div id="signature-container">
<canvas
id="signature-canvas"
class="signature-canvas"
ref="signRef"
height="150"
witdth="300"
></canvas>
</div>
<button @click="generateCanvas">生成Canvas图片</button>
<div id="canvaspic" ref="canvaspic">
<h3>Canvas图片👇</h3>
</div>
</template>
<style lang="scss">
.signature-canvas {
border: 1px solid black;
}
</style>
# 总结
- 通过光标的
clientX
和pageY
值,与canvas
自身与窗口左侧、顶部的宽高距离之差,计算出 起点 与 终点 的坐标。 pageY
与clientY
的不同点在于,前者包括 滚动条高度, 后者不包括。- 要考虑
JsPDF
生成的图片高度比A4
页面高度大的情况,以防 图片显示不完整 - 可为
canvas
包裹一层dom,添加适当的padding
以保证它显示时的观赏性。