需求:
最近我们项目增加了一个需求,需要实现在线电子签名功能,效果如下:
功能点:
- 用户可以在画板进行签名
- 具备清空功能
- 提交生成base64格式图片
- 生成图片可自定义宽高
- 生成图片可自定义旋转角度
- “此区域内请使用正楷签名“要在签名区Canvas底部,但是又不影响绘图区
解决思路:
-
底层是基于Canvas进行的画图,自己实现需要时间,先找现成的库进行需求开发吧。
-
引入signature_pad,github地址,能把1,2,3功能点完成。参考:代码1
-
自定义宽高,根据配置,传入宽高数据,重新生成base64图片,参考:代码2
-
自定义旋转角度,根据配置,传入旋转角度(这里只能生成垂直跟水平角度,不然不好计算),重新生成base64图片,参考:代码3
-
“此区域内请使用正楷签名“用css position布局,placeholder 在Index=-1, 外层canvas设置透明,能实现效果,参考:代码4
代码1:
import { ReactNode, createElement, useRef, useEffect } from "react";
import SignaturePad from "signature_pad";
useEffect(() => {
const jpgBackground = props.backgroundColor || "#fff";
signaturePad.current = new SignaturePad(canvasRef.current, {
minWidth: 0.5,
maxWidth: 2.5,
penColor: penColor || "black",
backgroundColor: imgType === "jpg" ? jpgBackground : "transparent"
});
// 等初始化函数后再进行canvas宽高赋值, 必须要进行这一步,不然canvas跟画布大小不一致
setTimeout(() => {
initCanvasSize();
}, 5)
}, []);
const initCanvasSize = (): void => {
const { clientWidth, clientHeight } = canvasRef.current;
canvasRef.current.width = clientWidth;
canvasRef.current.height = clientHeight;
signaturePad.current.clear();
};
// 清除
const handleCanvasClean = (): void => {
signaturePad.current.clear();
};
// 提交
const handleCanvasSubmit = async (): Promise<void> => {
if (signaturePad.current.isEmpty()) {
message.warning(emptySubmitTip || "请签名!");
return;
}
let dataURL = signaturePad.current.toDataURL();
if (rotate) {
dataURL = await rotateBase64Img("png", dataURL, Number(rotate));
}
if (imageWidth || imageHeight) {
dataURL = await proportionBase64Img("png", dataURL, Number(imageWidth), Number(imageHeight));
}
return dataURL;
};
// jsx
<div className="signature-canvas-container">
<div className="canvas-tips">{canvasPlaceholder || ""}</div>
<canvas className="signature-canvas" ref={canvasRef} />
</div>
代码2:
- 根据宽高压缩图, 根据原来的图重新生成一张新的图
- 使用API:ctx.drawImage(image, 0, 0, 原始图片宽, 原始图片高, 0, 0, 目标图片宽, 目标图片高)
- 缺点:尚未解决的 压缩太小,canvas位图会出现失真的情况。暂时没找到办法处理
// 根据宽高压缩图片
const proportionBase64Img = async (type: string, src: string, w?: number, h?: number): Promise<string> => {
const canvas = document.createElement("canvas");
const ctx: any = canvas.getContext("2d");
const image = new Image();
image.crossOrigin = "anonymous";
image.src = src;
let imgWidth;
let imgHeight;
return new Promise(resolve => {
image.onload = function () {
imgWidth = image.width;
imgHeight = image.height;
if (w) {
canvas.width = w;
canvas.height = (imgHeight * w) / imgWidth;
ctx.drawImage(image, 0, 0, imgWidth, imgHeight, 0, 0, w, (imgHeight * w) / imgWidth);
} else if (h) {
canvas.height = h;
canvas.width = (imgWidth * h) / imgHeight;
ctx.drawImage(image, 0, 0, imgWidth, imgHeight, 0, 0, (imgWidth * h) / imgHeight, h);
}
resolve(canvas.toDataURL(type));
};
});
};
代码3:
// 旋转图片
const rotateBase64Img = async (type: string, src: string, edg: number): Promise<string> => {
const canvas = document.createElement("canvas");
const ctx: any = canvas.getContext("2d");
let imgW; // 图片宽度
let imgH; // 图片高度
let size; // canvas初始大小
if (edg % 90 !== 0) {
console.error("旋转角度必须是90的倍数!");
return src;
}
const quadrant = (edg / 90) % 4; // 旋转象限
const cutCoor = { sx: 0, sy: 0, ex: 0, ey: 0 }; // 裁剪坐标
const image = new Image();
image.crossOrigin = "anonymous";
image.src = src;
return new Promise(resolve => {
image.onload = function () {
imgW = image.width;
imgH = image.height;
size = imgW > imgH ? imgW : imgH;
canvas.width = size * 2;
canvas.height = size * 2;
switch (quadrant) {
case 0:
cutCoor.sx = size;
cutCoor.sy = size;
cutCoor.ex = size + imgW;
cutCoor.ey = size + imgH;
break;
case 1:
cutCoor.sx = size - imgH;
cutCoor.sy = size;
cutCoor.ex = size;
cutCoor.ey = size + imgW;
break;
case 2:
cutCoor.sx = size - imgW;
cutCoor.sy = size - imgH;
cutCoor.ex = size;
cutCoor.ey = size;
break;
case 3:
cutCoor.sx = size;
cutCoor.sy = size - imgW;
cutCoor.ex = size + imgH;
cutCoor.ey = size + imgW;
break;
}
ctx.translate(size, size);
ctx.rotate((edg * Math.PI) / 180);
ctx.drawImage(image, 0, 0);
const imgData = ctx.getImageData(cutCoor.sx, cutCoor.sy, cutCoor.ex, cutCoor.ey);
if (quadrant % 2 === 0) {
canvas.width = imgW;
canvas.height = imgH;
} else {
canvas.width = imgH;
canvas.height = imgW;
}
ctx.putImageData(imgData, 0, 0);
resolve(canvas.toDataURL(type));
};
});
};
代码4:
.canvas-tips {
width: 0.24rem;
font-size: 0.24rem;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: bold;
color: rgba(0, 0, 0, 0.12);
-webkit-background-clip: text;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: -1;
}