Skip to content

React+signature_pad实现Web签名板

Published:

签名板方案使用signature_pad这个第三方库,该库方案基于 Canvas。

安装

pnpm add signature_pad

使用

以下是一个基本的使用示例:

import React from "react";
import SignaturePad from "signature_pad";

const buttonClsx =
  "bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded";

export default () => {
  const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
  /** 签名板实例 */
  const padRef = React.useRef<SignaturePad>();

  React.useEffect(() => {
    if (!canvasRef.current) return;
    // 处理高清屏适配
    const ratio = Math.max(window.devicePixelRatio || 1, 1);
    canvasRef.current.width = canvasRef.current.offsetWidth * ratio;
    canvasRef.current.height = canvasRef.current.offsetHeight * ratio;
    canvasRef.current?.getContext("2d")?.scale(ratio, ratio);

    // 实例化签名板
    padRef.current = new SignaturePad(canvasRef.current, {
      backgroundColor: "rgba(0,0,0,0)",
    });
  }, []);

  const handleUndo = () => {
    // 获取签名的点数组
    const data = padRef.current?.toData();
    if (data) {
      // 移除最后一笔数据
      data.pop();
      // 根据新的数据形成签名
      padRef.current?.fromData(data);
    }
  };

  const handleRewrite = () => {
    padRef.current?.clear();
  };

  const exportAsBase64 = () => {
    if (padRef.current?.isEmpty()) {
      alert("请先签名");
      return;
    }
    // 返回png的base64编码
    const dataUrl = padRef.current?.toDataURL("image/png");
    console.log("@dataUrl", dataUrl);
    return dataUrl;
  };

  const exportAsSVG = () => {
    if (padRef.current?.isEmpty()) {
      alert("请先签名");
      return;
    }
    const svg = padRef.current?.toSVG({ includeBackgroundColor: true });
    console.log("@svg", svg);
    return svg;
  };

  return (
    <div className="w-screen h-screen relative">
      <div className="absolute inset-0 w-full h-full  bg-white z-500 flex flex-col items-center justify-center overflow-auto">
        <canvas
          ref={canvasRef}
          className="border border-gray-300 w-full h-full"
        />
        <br />

        <div className="grid gap-2 grid-rows-2 grid-cols-2">
          <button className={buttonClsx} onClick={handleUndo}>
            撤销
          </button>
          <button className={buttonClsx} onClick={handleRewrite}>
            重写
          </button>
          <button className={buttonClsx} onClick={exportAsBase64}>
            导出为base64
          </button>
          <button className={buttonClsx} onClick={exportAsSVG}>
            导出为svg
          </button>
        </div>
        <br />
      </div>
    </div>
  );
};

常见问题处理

背景颜色设置

可以在实例化时配置背景颜色:

padRef.current = new SignaturePad(canvasRef.current, {
  // 透明
  // backgroundColor: "rgba(0,0,0,0)",
  // 白色
  backgroundColor: "rgba(255, 255, 255, 1)",
  // 红色
  // backgroundColor: "rgba(255,0,0,1)",
});

注意:

如何旋转图片

/**
 * 旋转图片并返回适配大小的canvas
 * @param {HTMLImageElement} imageElement - 需要旋转的图片元素
 * @param {number} angle - 旋转角度(度数)
 * @returns {HTMLCanvasElement} 返回旋转后的canvas元素
 * @throws {Error} 当参数无效时抛出错误
 */
function rotateAndFit(imageElement, angle) {
  // 参数验证
  if (!(imageElement instanceof HTMLImageElement)) {
    throw new Error("First parameter must be an Image element");
  }
  if (!Number.isFinite(angle)) {
    throw new Error("Angle must be a valid number");
  }

  // 确保图片已加载完成
  if (!imageElement.complete || !imageElement.naturalWidth) {
    throw new Error("Image has not finished loading");
  }

  try {
    // 将角度标准化到 0-360 度范围内
    const normalizedAngle = ((angle % 360) + 360) % 360;

    // 转换角度为弧度
    const radian = (normalizedAngle * Math.PI) / 180;

    // 计算旋转变换的正弦和余弦值
    const sin = Math.abs(Math.sin(radian));
    const cos = Math.abs(Math.cos(radian));

    // 获取原始图片尺寸
    const imgWidth = imageElement.width;
    const imgHeight = imageElement.height;

    // 计算旋转后的新尺寸
    const newWidth = Math.ceil(imgWidth * cos + imgHeight * sin);
    const newHeight = Math.ceil(imgWidth * sin + imgHeight * cos);

    // 创建新的canvas元素
    const canvas = document.createElement("canvas");
    canvas.width = newWidth;
    canvas.height = newHeight;

    // 获取2D渲染上下文
    const ctx = canvas.getContext("2d", {
      alpha: true, // 是否需要透明通道
      willReadFrequently: false, // 是否需要频繁读取像素数据
    });

    if (!ctx) {
      throw new Error("Failed to get canvas context");
    }

    // 设置图像平滑
    ctx.imageSmoothingEnabled = true;
    ctx.imageSmoothingQuality = "high";

    // 保存当前上下文状态
    ctx.save();

    // 移动坐标系原点到画布中心
    ctx.translate(newWidth / 2, newHeight / 2);

    // 执行旋转变换
    ctx.rotate(radian);

    // 绘制图片,使其居中
    ctx.drawImage(
      imageElement,
      -imgWidth / 2, // 向左偏移图片宽度的一半
      -imgHeight / 2, // 向上偏移图片高度的一半
      imgWidth,
      imgHeight
    );

    // 恢复上下文状态
    ctx.restore();

    return canvas;
  } catch (error) {
    console.error("Error in rotateAndFit:", error);
    throw error;
  }
}

// 使用示例:
/*
const img = new Image();
img.onload = function() {
    try {
        const rotatedCanvas = rotateAndFit(img, 45);
        document.body.appendChild(rotatedCanvas);
    } catch (error) {
        console.error('Failed to rotate image:', error);
    }
};
img.src = 'your-image-url.jpg';
*/

画板点位校准

如遇到签名位置不准确的问题,请检查:

SVG 导出不完整

如果导出的 SVG 不完整但 PNG 正常,检查:

API 参考

{
  minWidth: 0.5,     // 最小线宽
  maxWidth: 2.5,     // 最大线宽
  throttle: 16,      // 绘制频率(ms)
  minDistance: 5,    // 最小绘制距离
  backgroundColor: "rgba(0,0,0,0)",  // 背景色
  penColor: "black", // 画笔颜色
  velocityFilterWeight: 0.7  // 速度平滑系数
}
  clear() - 清空画板
  isEmpty() - 检查是否为空
  toData() - 获取点位数据
  fromData() - 根据点位数据重绘
  toDataURL() - 导出为图片
  toSVG() - 导出为 SVG

Previous Post
Web文件上传
Next Post
Nginx中启用https