通过js + canvas实现签名效果
在init方法中构建canvas实例,并绑定相应的鼠标事件,通过单独的事件绑定方法,将三种鼠标事件处理逻辑分割开来。
init() {
let canvas = document.querySelector(this._id);
this.ctx = canvas.getContext('2d');
this.canvas = canvas;
this._width = canvas.width;
this._height = canvas.height;
this._bindMethod('mousedown', this._start);
this._bindMethod('mousemove', this._move);
this._bindMethod('mouseup', this._end);
}
鼠标按下时,获取当前的当前的坐标,并记录当前开始坐标,在此设置线样式。
_start({offsetX, offsetY}) {
this._isStart = true;
this.ctx.beginPath();
this._startX = offsetX;
this._startY = offsetY;
this.ctx.moveTo(offsetX, offsetY);
this._setLineStyle();
}
鼠标移动时,记录每一次移动的坐标信息,也就是路径点位信息。
_move({offsetX, offsetY}) {
if (!this._isStart) {
return;
}
this.ctx.lineTo(offsetX, offsetY);
this._endX = offsetX;
this._endY = offsetY;
this._currentPath.push([offsetX, offsetY]);
this.ctx.stroke();
}
移动结束后,记录这一次总体路径信息,重置相关属性。
this._isStart = false;
this.path.push({
startX: this._startX,
startY: this._startY,
path: this._currentPath
});
this._clearData();
设置线宽,通过类型转化判断当前参数类型是否为数字类型或字符串数字。
setLineWidth(lineWidth = 1) {
if (isNaN(+lineWidth)) {
alert('数据类型不是数字类型或字符串数字');
throw new Error('数据类型不是数字类型或字符串数字');
}
this._lineWidth = lineWidth;
}
设置线颜色。
setLineColor(lineColor = '#000') {
this._lineColor = lineColor;
}
撤回功能,根据路径记录信息,重新绘制图形。
withdraw() {
let length = this.path.length;
if (!length) {
this.reset()
}
this._clearCanvas();
this.path.splice(length - 1, 1);
this.path.forEach(({startX, startY, path}) => {
this.ctx.beginPath();
this._setLineStyle();
this.ctx.moveTo(startX, startY);
path.forEach(([endX, endY]) => {
this.ctx.lineTo(endX, endY);
})
this.ctx.stroke();
});
}
下载功能。
downLoad() {
let base64URL = this.canvas.toDataURL('image/png');
const a = document.createElement('a');
a.download = '签名';
const mimeType = base64URL.match(/:(.*?);/)[1];
const byteCharacters = atob(base64URL.split(',')[1]);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: mimeType });
a.href = URL.createObjectURL(blob);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
注意
坐标选取处理(IE下没有pageX,pageY)
pageX,pageY:鼠标指针的位置,文档的坐标。(也就是相对于浏览器窗口),会受css样式偏移量的影响,如padding,margin等
offsetX,offsetY:鼠标相对于事件源元素的x,y坐标。
完整代码
sign.js插件代码
class SignCanvas {
constructor(id) {
this.canvas = null;
this.ctx = null;
this._id = id;
this._width = 0;
this._height = 0;
this._startX = null;
this._startY = null;
this._endX = null;
this._endY = null;
this.path = [];
this._currentPath = [];
this._isStart = false;
this._lineWidth = 1;
this._lineColor = '#000';
}
init() {
let canvas = document.querySelector(this._id);
this.ctx = canvas.getContext('2d');
this.canvas = canvas;
this._width = canvas.width;
this._height = canvas.height;
this._bindMethod('mousedown', this._start);
this._bindMethod('mousemove', this._move);
this._bindMethod('mouseup', this._end);
}
_bindMethod(key, method) {
this.canvas.addEventListener(key, (e) => {
method.call(this, e);
})
}
_start({offsetX, offsetY}) {
this._isStart = true;
this.ctx.beginPath();
this._startX = offsetX;
this._startY = offsetY;
this.ctx.moveTo(offsetX, offsetY);
this._setLineStyle();
}
_move({offsetX, offsetY}) {
if (!this._isStart) {
return;
}
this.ctx.lineTo(offsetX, offsetY);
this._endX = offsetX;
this._endY = offsetY;
this._currentPath.push([offsetX, offsetY]);
this.ctx.stroke();
}
_end() {
this._isStart = false;
this.path.push({
startX: this._startX,
startY: this._startY,
path: this._currentPath
});
this._clearData();
}
_setLineStyle() {
this.ctx.lineWidth = this._lineWidth;
this.ctx.strokeStyle = this._lineColor;
}
_clearData() {
this._startX = null;
this._startY = null;
this._endX = null;
this._endY = null;
this._currentPath = [];
}
_clearCanvas() {
this.ctx.clearRect(0, 0, this._width, this._height);
}
setLineWidth(lineWidth = 1) {
if (isNaN(+lineWidth)) {
alert('数据类型不是数字类型或字符串数字');
throw new Error('数据类型不是数字类型或字符串数字');
}
this._lineWidth = lineWidth;
}
setLineColor(lineColor = '#000') {
this._lineColor = lineColor;
}
reset() {
this._clearCanvas();
this._clearData();
this.path = [];
}
withdraw() {
let length = this.path.length;
if (!length) {
this.reset()
}
this._clearCanvas();
this.path.splice(length - 1, 1);
this.path.forEach(({startX, startY, path}) => {
this.ctx.beginPath();
this._setLineStyle();
this.ctx.moveTo(startX, startY);
path.forEach(([endX, endY]) => {
this.ctx.lineTo(endX, endY);
})
this.ctx.stroke();
});
}
downLoad() {
let base64URL = this.canvas.toDataURL('image/png');
const a = document.createElement('a');
a.download = '签名';
const mimeType = base64URL.match(/:(.*?);/)[1];
const byteCharacters = atob(base64URL.split(',')[1]);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: mimeType });
a.href = URL.createObjectURL(blob);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
}
export default SignCanvas;
html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>canvas签字</title>
</head>
<style>
html, body {
/*padding: 0;*/
/*margin: 0;*/
}
</style>
<body>
<div>
<button id="withdraw">撤回</button>
<button id="downLoad">下载</button>
</div>
<canvas id="canvas" width="400" height="400" style="border: 1px solid #7d7676; margin-top: 10px">
该浏览器不支持canvas标签
</canvas>
<script type="module">
import SignCanvas from './sign.js';
let sign = new SignCanvas('#canvas');
sign.init();
let btn = document.querySelector('#withdraw');
let btnDown = document.querySelector('#downLoad');
btn.addEventListener('click', () => {
sign.withdraw();
});
btnDown.addEventListener('click', () => {
sign.downLoad();
})
</script>
</body>
</html>