canvas 实现画布签名功能
方法描述
在 Web 开发中,实现画布签名功能是一个常见的需求,例如电子合同签署、在线表单签名等。 这个Signature方法,它可以在画布上实现签名功能,并提供了缩放、旋转、保存等实用方法。
代码概述
Signature 主要实现了以下功能
- 在 HTML5 画布上绘制签名。
- 支持缩放、旋转画布。
- 保存签名为 PNG 或 JPG 格式的图片。
- 清空画布、清除上一笔签名
Signature
类的原型方法提供了各种操作画布的功能,如缩放、旋转、保存、清空等。每个方法都有明确的功能和参数,方便开发者使用
使用实例 (这里使用的是vue)
在这里插入代码片
<!-- -->
<template>
<div class="box">
<canvas ref="canvas" class="canvas"></canvas>
<div class="clearBtn" @click="clear"> 全部清除</div>
<div class="clearBtn" @click="clearLastStroke"> 清除上一笔</div>
</div>
</template>
<script>
import Signature from "./signature";
import { Dialog, Toast } from "vant";
let signature = null;
export default {
mounted() {
setTimeout(() => {
signature = new Signature(this.$refs.canvas);
}, 100);
},
methods: {
// 逐笔删除
clearLastStroke(){
// 调用逐笔清除方法
signature.clearLastStroke();
},
clear() {
signature.clear();
},
},
};
</script>
构造函数
在这里插入代码片
function Signature(canvas, degree = 0, config = {}) {
if (!(this instanceof Signature)) return new Signature(canvas, config);
if (!canvas) return;
let { width, height } = window.getComputedStyle(canvas, null);
width = width.replace("px", "");
height = height.replace("px", "");
this.canvas = canvas;
this.context = canvas.getContext("2d");
this.width = width;
this.height = height;
const context = this.context;
const devicePixelRatio = window.devicePixelRatio || 1;
if (devicePixelRatio) {
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
canvas.height = height * devicePixelRatio;
canvas.width = width * devicePixelRatio;
context.scale(devicePixelRatio, devicePixelRatio);
} else {
canvas.width = width;
canvas.height = height;
}
context.lineWidth = 5;
context.strokeStyle = "black";
context.lineCap = "round";
context.lineJoin = "round";
Object.assign(context, config);
const { left, top } = canvas.getBoundingClientRect();
const point = {};
// 检测是否为鸿蒙系统 NEXT
const isHarmonyNext = /HarmonyOS/i.test(navigator.userAgent) && /Next/i.test(navigator.userAgent);
const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent) || isHarmonyNext;
if (!isMobile) {
context.shadowBlur = 1;
context.shadowColor = "black";
}
let pressed = false;
this.hasContent = false;
this.paths = [];
let currentPath = [];
this.drawPlaceholder = () => {
const placeholderText = "请用正楷签名";
this.context.font = "20px Arial";
this.context.fillStyle = "#aaabb9";
this.context.textAlign = "left";
this.context.fillText(placeholderText, 12, 30);
};
this.drawPlaceholder();
const paint = signal => {
if (!this.hasContent) {
context.clearRect(0, 0, width, height);
this.hasContent = true;
}
switch (signal) {
case 1:
context.beginPath();
context.moveTo(point.x, point.y);
currentPath = []; // 开始新的一笔签名,清空当前路径
currentPath.push({ x: point.x, y: point.y });
case 2:
context.lineTo(point.x, point.y);
context.stroke();
currentPath.push({ x: point.x, y: point.y });
break;
default:
}
};
const create = signal => e => {
e.preventDefault();
if (signal === 1) {
pressed = true;
}
if (signal === 1 || pressed) {
if (isHarmonyNext && e.type.includes('touch')) {// 适配鸿蒙系统 NEXT 触摸事件
e = e.changedTouches[0];
} else {
e = isMobile ? e.touches[0] : e;
}
point.x = e.clientX - left;
point.y = e.clientY - top;
paint(signal);
if (signal === 2) {
this.paths[this.paths.length - 1] = currentPath;
}
}
if (signal === 1) {
this.paths.push(currentPath);
}
};
const start = create(1);
const move = create(2);
const requestAnimationFrame = window.requestAnimationFrame;
const optimizedMove = requestAnimationFrame
? e => {
requestAnimationFrame(() => {
move(e);
});
}: move;
if (isMobile) {
canvas.addEventListener("touchstart", start);
canvas.addEventListener("touchmove", optimizedMove);
} else {
canvas.addEventListener("mousedown", start);
canvas.addEventListener("mousemove", optimizedMove);
["mouseup", "mouseleave"].forEach(event => {
canvas.addEventListener(event, () => {
pressed = false;
});
});
}
if (typeof degree === "number") {
this.degree = degree;
context.rotate((degree * Math.PI) / 180);
switch (degree) {
case -90:
context.translate(-height, 0);
break;
case 90:
context.translate(0, -width);
break;
case -180:
case 180:
context.translate(-width, -height);
break;
default:
}
}
}
export default Signature;
原型方法
在这里插入代码片
Signature.prototype = {
scale(width, height, canvas = this.canvas) {
const w = canvas.width;
const h = canvas.height;
width = width || w;
height = height || h;
if (width !== w || height !== h) {
const tmpCanvas = document.createElement("canvas");
const tmpContext = tmpCanvas.getContext("2d");
tmpCanvas.width = width;
tmpCanvas.height = height;
tmpContext.drawImage(canvas, 0, 0, w, h, 0, 0, width, height);
canvas = tmpCanvas;
}
return canvas;
},
isSigned() {
return !this.hasContent;
},
rotate(degree, image = this.canvas) {
degree = ~~degree;
if (degree !== 0) {
const maxDegree = 180;
const minDegree = -90;
if (degree > maxDegree) {
degree = maxDegree;
} else if (degree < minDegree) {
degree = minDegree;
}
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const height = image.height;
const width = image.width;
const degreePI = (degree * Math.PI) / 180;
switch (degree) {
case -90:
canvas.width = height;
canvas.height = width;
context.rotate(degreePI);
context.drawImage(image, -width, 0);
break;
case 90:
canvas.width = height;
canvas.height = width;
context.rotate(degreePI);
context.drawImage(image, 0, -height);
break;
case 180:
canvas.width = width;
canvas.height = height;
context.rotate(degreePI);
context.drawImage(image, -width, -height);
break;
default:
}
image = canvas;
}
return image;
},
getPNGImage(canvas = this.canvas) {
return canvas.toDataURL("image/png");
},
getJPGImage(canvas = this.canvas) {
return canvas.toDataURL("image/jpeg", 0.5);
},
downloadPNGImage(image) {
const url = image.replace(
"image/png",
"image/octet-stream;Content-Disposition:attachment;filename=test.png"
);
window.location.href = url;
},
dataURLtoBlob(dataURL) {
const arr = dataURL.split(",");
const mime = arr[0].match(/:(.*?);/)[1];
const bStr = atob(arr[1]);
let n = bStr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bStr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
},
getCanvasDimensions () {
if ([ -90, 90, ].includes(this.degree)) {
return { width: this.height, height: this.width };
}
return { width: this.width, height: this.height };
},
clear () {
const { width, height } = this.getCanvasDimensions();
this.context.clearRect(0, 0, width, height);
this.hasContent = false;
// this.paths = [];
this.drawPlaceholder();
},
upload(blob, url, success, failure) {
const formData = new FormData();
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
formData.append("image", blob, "sign");
xhr.open("POST", url, true);
xhr.onload = () => {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
success(xhr.responseText);
} else {
failure();
}
};
xhr.onerror = e => {
if (typeof failure === "function") {
failure(e);
} else {
console.log(`upload img error: ${e}`);
}
};
xhr.send(formData);
},
clearLastStroke() {
if (this.paths.length > 0) {
this.paths.pop();
this.context.clearRect(0, 0, this.width, this.height);
this.hasContent = false;
for (let path of this.paths) {
if (path.length > 0) {
this.context.beginPath();
this.context.moveTo(path[0].x, path[0].y);
for (let i = 1; i < path.length; i++) {
this.context.lineTo(path[i].x, path[i].y);
}
this.context.stroke();
this.hasContent = true;
}
}
}
if(!this.hasContent){
this.drawPlaceholder();
}
},
};