# 电子签名

# 场景介绍

​ 项目开发中,业务需求可能会要求用户在一些协议中签字,这时前端就要用到canvas绘制图形,并将它生成图片,上传到服务器上。本篇主要用canvas搭配html2CanvasJsPDF两个工具库去实现它。

TIP

本篇实现环境:vue3.2

# 实现

# canvas绘制

# 绘制事件

  • gloablAlpha 不透明度
  • lineWidth 线宽
  • strokeStyle 绘制样式
  • moveTolineTo起点与落点
  • beginPathclosePath 开始路径、关闭路径
  • 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 根据A4canvas宽度,等比例缩放 canvas高度。(此时往小缩)
  • let pageHeight = (canvasWidth / 595.28) * 841.89 根据A4canvas宽度,等比例缩放 页面高度。(此时往大放)
  • 如果 页面高度 大于 图片高度,则直接生成图片
  • 如果 图片高度 大于 页面高度,则在生成图片的同时,增加空白页。且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>


# 总结

  1. 通过光标的clientXpageY值,与canvas自身与窗口左侧、顶部的宽高距离之差,计算出 起点终点 的坐标。
  2. pageYclientY 的不同点在于,前者包括 滚动条高度, 后者不包括。
  3. 要考虑JsPDF生成的图片高度比A4页面高度大的情况,以防 图片显示不完整
  4. 可为canvas包裹一层dom,添加适当的padding以保证它显示时的观赏性。

# 参考文章

  1. JS基础:clientX、pageX、screenX、offsetX、clientWidth、offsetWidth 详解 - 掘金 (juejin.cn) (opens new window)
  2. Options | html2canvas (hertzen.com) (opens new window)
  3. Home - Documentation (githack.com) (opens new window)