在线签字常见于合同确认、电子表格和文件签署等场景,原文用 HTML5 Canvas 封装了一个 Draw 类,支持鼠标与触摸书写、线条配置、撤销、清除以及导出图片。下面按代码实现拆解其设计思路和关键细节。
一、Draw 类的构造与高清适配
Draw 接收一个参数对象,可用 el 传入已有 canvas,未传则动态创建。核心参数包括 width、height、lineWidth、strokeStyle,默认尺寸 400×400、线宽 1、颜色 #333333。构造时把 canvas 的实际宽高设置为参数值的 2 倍,再通过 ctx.scale(2,2) 缩放绘图上下文,这样在高清屏上绘制更清晰。随后设置 lineWidth、lineJoin 和 lineCap 为 round,使笔画连接和端点更自然,并调用 init 绑定事件。
- function Draw(params = {}) {
- this.el = params.el || document.createElement('canvas');
- this.state = {
- undopath: [], index: -1, old: void 0, isStart: false,
- width: params.width || 400, height: params.height || 400,
- lineWidth: params.lineWidth || 1,
- isTouch: 'ontouchstart' in window,
- strokeStyle: params.strokeStyle || '#333333'
- };
- this.el.width = this.state.width * 2;
- this.el.height = this.state.height * 2;
- document.body.appendChild(this.el);
- this.ctx = this.el.getContext('2d');
- this.ctx.scale(2, 2);
- this.ctx.lineWidth = this.state.lineWidth;
- this.ctx.lineJoin = 'round';
- this.ctx.lineCap = 'round';
- this.init();
- }
复制代码
二、事件绑定与坐标处理
init 根据 isTouch 判断设备,触摸设备绑定 touchstart、touchmove、touchend、touchcancel,非触摸设备绑定 mousedown、mousemove、mouseup、mouseout。onStart 增加笔画索引并置 isStart 为 true;onMove 中先 preventDefault 避免触摸滚动,未开始书写则直接返回。坐标由 pos 方法获取:触摸事件取 touches[0].pageX/pageY,鼠标事件取 offsetX/2、offsetY/2。由于画布逻辑尺寸被放大 2 倍并缩放了上下文,鼠标坐标需要除以 2 才能对应到绘制坐标。
- Draw.prototype.onMove = function (e) {
- e.preventDefault();
- if (!this.state.isStart) return;
- var pos = this.pos(e);
- var index = this.state.index;
- this.ctx.strokeStyle = this.state.strokeStyle;
- if (this.state.old) {
- this.ctx.beginPath();
- this.ctx.moveTo(this.state.old.x, this.state.old.y);
- this.ctx.lineTo(pos.x, pos.y);
- this.ctx.stroke();
- }
- this.state.old = pos;
- if (this.state.undopath[index]) {
- this.state.undopath[index].push({ x: this.state.old.x, y: this.state.old.y });
- } else {
- this.state.undopath[index] = [{ x: this.state.old.x, y: this.state.old.y, strokeStyle: this.ctx.strokeStyle, lineWidth: this.ctx.lineWidth }];
- }
- };
复制代码
三、笔画存储、撤销和清除
每一条笔画用 undopath 按下标保存点集,第一笔点还记录 strokeStyle 和 lineWidth,后续点只存坐标。onEnd 时将 old 置空、isStart 置 false。undo 会先递减 index,弹出 undopath 中最后一笔,然后 clearRect 清空画布,再遍历剩余笔画重新 beginPath、moveTo、lineTo 和 stroke,从而恢复撤销后的画面。clear 则把 index 重置为 -1、undopath 置空,并调用 clearRect 清空整个 canvas。
- Draw.prototype.undo = function () {
- this.state.index >= 0 && --this.state.index;
- var undopath = this.state.undopath;
- this.state.undopath.pop();
- this.ctx.clearRect(0, 0, this.el.width, this.el.height);
- if (undopath) {
- this.ctx.beginPath();
- for (var z = 0; z < undopath.length; ++z) {
- this.ctx.moveTo(undopath[z][0].x, undopath[z][0].y);
- this.ctx.lineWidth = undopath[z][0].lineWidth;
- this.ctx.strokeStyle = undopath[z][0].strokeStyle;
- for (var i = 0; i < undopath[z].length; ++i) {
- this.ctx.lineTo(undopath[z][i].x, undopath[z][i].y);
- }
- }
- this.ctx.stroke();
- this.ctx.closePath();
- } else {
- this.state.undopath = [];
- }
- };
复制代码
四、导出图片与页面调用
ouput 方法直接返回 this.el.toDataURL(),得到 data URL 形式的图片。页面初始化时 new Draw 传入线宽、宽高和颜色,按钮分别绑定生成图片、清除和撤销。撤销前判断 undopath.length 是否大于 0,避免没有签名时继续操作。生成图片时创建 Image,把 src 设为 sign.ouput(),加载后插入页面,并移除上一次生成的 img。
- var sign = new Draw({
- lineWidth: 10,
- width: 400,
- height: 400,
- strokeStyle: '#333333'
- });
- document.querySelector('.ouput').onclick = function () {
- var img = new Image();
- img.style.width = '200px';
- img.src = sign.ouput();
- img.onload = function () {
- document.body.appendChild(img);
- };
- document.querySelector('img') && document.querySelector('img').remove();
- };
- document.querySelector('.clear').onclick = function () {
- sign.clear();
- };
- document.querySelector('.undo').onclick = function () {
- if (sign.state.undopath.length > 0) {
- sign.undo();
- } else {
- console.log('还没有签名');
- }
- };
复制代码
五、兼容性与注意点
原文通过 UMD 包装暴露 Draw,兼容 CommonJS、AMD 和全局变量。触摸坐标直接使用 pageX/pageY,鼠标坐标使用 offsetX/2、offsetY/2,若 canvas 不在页面左上角或布局复杂,实际项目应结合 getBoundingClientRect 换算相对坐标。destroyed 中用 bind 生成的新函数移除监听,和 init 绑定时生成的新函数不是同一引用,因此这种写法不能真正移除监听;若要彻底销毁,应保存绑定后的函数引用再 removeEventListener。撤销重绘依赖 undopath 保存完整点集,签名时间较长时点数据会持续增长,需要关注内存占用和重绘性能。 |