签名板方案使用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)",
});
注意:
- 导出为 dataURL 时背景色会自动包含。
- 导出 SVG 时需要额外设置
includeBackgroundColor: true
如何旋转图片
/**
* 旋转图片并返回适配大小的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';
*/
画板点位校准
如遇到签名位置不准确的问题,请检查:
- 是否在初始化前正确设置 canvas 尺寸
- 页面是否进行了缩放
- 是否正确处理了设备像素比(devicePixelRatio)
SVG 导出不完整
如果导出的 SVG 不完整但 PNG 正常,检查:
- canvas 的 width 和 height 时是否乘以
window.devicePixelRatio
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