查看: 253|回复: 3

HTML5 Canvas手写签字板实现与撤销清除导出图片

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在线签字常见于合同确认、电子表格和文件签署等场景,原文用 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 绑定事件。
  1. function Draw(params = {}) {
  2.   this.el = params.el || document.createElement('canvas');
  3.   this.state = {
  4.     undopath: [], index: -1, old: void 0, isStart: false,
  5.     width: params.width || 400, height: params.height || 400,
  6.     lineWidth: params.lineWidth || 1,
  7.     isTouch: 'ontouchstart' in window,
  8.     strokeStyle: params.strokeStyle || '#333333'
  9.   };
  10.   this.el.width = this.state.width * 2;
  11.   this.el.height = this.state.height * 2;
  12.   document.body.appendChild(this.el);
  13.   this.ctx = this.el.getContext('2d');
  14.   this.ctx.scale(2, 2);
  15.   this.ctx.lineWidth = this.state.lineWidth;
  16.   this.ctx.lineJoin = 'round';
  17.   this.ctx.lineCap = 'round';
  18.   this.init();
  19. }
复制代码

二、事件绑定与坐标处理

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 才能对应到绘制坐标。
  1. Draw.prototype.onMove = function (e) {
  2.   e.preventDefault();
  3.   if (!this.state.isStart) return;
  4.   var pos = this.pos(e);
  5.   var index = this.state.index;
  6.   this.ctx.strokeStyle = this.state.strokeStyle;
  7.   if (this.state.old) {
  8.     this.ctx.beginPath();
  9.     this.ctx.moveTo(this.state.old.x, this.state.old.y);
  10.     this.ctx.lineTo(pos.x, pos.y);
  11.     this.ctx.stroke();
  12.   }
  13.   this.state.old = pos;
  14.   if (this.state.undopath[index]) {
  15.     this.state.undopath[index].push({ x: this.state.old.x, y: this.state.old.y });
  16.   } else {
  17.     this.state.undopath[index] = [{ x: this.state.old.x, y: this.state.old.y, strokeStyle: this.ctx.strokeStyle, lineWidth: this.ctx.lineWidth }];
  18.   }
  19. };
复制代码

三、笔画存储、撤销和清除

每一条笔画用 undopath 按下标保存点集,第一笔点还记录 strokeStyle 和 lineWidth,后续点只存坐标。onEnd 时将 old 置空、isStart 置 false。undo 会先递减 index,弹出 undopath 中最后一笔,然后 clearRect 清空画布,再遍历剩余笔画重新 beginPath、moveTo、lineTo 和 stroke,从而恢复撤销后的画面。clear 则把 index 重置为 -1、undopath 置空,并调用 clearRect 清空整个 canvas。
  1. Draw.prototype.undo = function () {
  2.   this.state.index >= 0 && --this.state.index;
  3.   var undopath = this.state.undopath;
  4.   this.state.undopath.pop();
  5.   this.ctx.clearRect(0, 0, this.el.width, this.el.height);
  6.   if (undopath) {
  7.     this.ctx.beginPath();
  8.     for (var z = 0; z < undopath.length; ++z) {
  9.       this.ctx.moveTo(undopath[z][0].x, undopath[z][0].y);
  10.       this.ctx.lineWidth = undopath[z][0].lineWidth;
  11.       this.ctx.strokeStyle = undopath[z][0].strokeStyle;
  12.       for (var i = 0; i < undopath[z].length; ++i) {
  13.         this.ctx.lineTo(undopath[z][i].x, undopath[z][i].y);
  14.       }
  15.     }
  16.     this.ctx.stroke();
  17.     this.ctx.closePath();
  18.   } else {
  19.     this.state.undopath = [];
  20.   }
  21. };
复制代码

四、导出图片与页面调用

ouput 方法直接返回 this.el.toDataURL(),得到 data URL 形式的图片。页面初始化时 new Draw 传入线宽、宽高和颜色,按钮分别绑定生成图片、清除和撤销。撤销前判断 undopath.length 是否大于 0,避免没有签名时继续操作。生成图片时创建 Image,把 src 设为 sign.ouput(),加载后插入页面,并移除上一次生成的 img。
  1. var sign = new Draw({
  2.   lineWidth: 10,
  3.   width: 400,
  4.   height: 400,
  5.   strokeStyle: '#333333'
  6. });
  7. document.querySelector('.ouput').onclick = function () {
  8.   var img = new Image();
  9.   img.style.width = '200px';
  10.   img.src = sign.ouput();
  11.   img.onload = function () {
  12.     document.body.appendChild(img);
  13.   };
  14.   document.querySelector('img') && document.querySelector('img').remove();
  15. };
  16. document.querySelector('.clear').onclick = function () {
  17.   sign.clear();
  18. };
  19. document.querySelector('.undo').onclick = function () {
  20.   if (sign.state.undopath.length > 0) {
  21.     sign.undo();
  22.   } else {
  23.     console.log('还没有签名');
  24.   }
  25. };
复制代码

五、兼容性与注意点

原文通过 UMD 包装暴露 Draw,兼容 CommonJS、AMD 和全局变量。触摸坐标直接使用 pageX/pageY,鼠标坐标使用 offsetX/2、offsetY/2,若 canvas 不在页面左上角或布局复杂,实际项目应结合 getBoundingClientRect 换算相对坐标。destroyed 中用 bind 生成的新函数移除监听,和 init 绑定时生成的新函数不是同一引用,因此这种写法不能真正移除监听;若要彻底销毁,应保存绑定后的函数引用再 removeEventListener。撤销重绘依赖 undopath 保存完整点集,签名时间较长时点数据会持续增长,需要关注内存占用和重绘性能。
回复

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: HTML5 Canvas手写签字板实现与撤销清除导出图片

这个签字板的封装思路挺完整的,高清屏 2 倍适配、鼠标触摸统一处理、点集存储后重绘撤销,结构比较清楚,适合直接拿来改造成合同签署或电子表格确认场景。几个细节可以再打磨一下:撤销重绘那段里内层循环的 undopath.x 和 undopath.y 看起来像是笔误,应该按 i 取点;触摸坐标用 pageX/pageY 时最好减去 canvas 的位置偏移,并且和鼠标坐标一样除以 2,否则在页面有滚动或画布不在左上角时会有偏差;重绘时每笔的 lineWidth 和 strokeStyle 最好按笔画存储,而不是统一取 undopath[0],这样多笔不同样式也能正确恢复。另外 clear 时除了清空 undopath 和 index,也建议把 old、isStart 一起重置,避免连续操作时状态残留。导出图片用 toDataURL 很方便,但要注意画布默认透明背景,实际签署场景可能需要先铺一层白色底。正文里 clear 和导出部分似乎还没展开,期待后续补全。
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: HTML5 Canvas手写签字板实现与撤销清除导出图片

看完楼主的拆解,整体思路挺清晰的,高清屏适配用实际宽高乘 2 再 scale 2 的做法很实用,坐标处理也解释得到位。撤销部分通过保存点集、清空后重绘来恢复画面,这个方向没问题。提两个实现上的小提醒:重绘每一条笔画时最好单独走 beginPath 和 stroke,并恢复该笔画当时的线宽和颜色,避免多条笔画样式互相影响;导出图片时留意 canvas 实际尺寸是逻辑尺寸的 2 倍,按需缩放回去会更符合预期。触摸坐标如果页面有滚动,结合 canvas 的位置做一下换算会更稳。整体是个比较完整的签字板实现思路。
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: HTML5 Canvas手写签字板实现与撤销清除导出图片

这个签字板封装思路挺清晰的,高清屏用 2 倍实际尺寸再 scale(2,2) 确实能让笔画更细腻,鼠标和触摸事件分开绑定也考虑到了移动端场景。撤销部分用 undopath 保存每笔点位、清空后重绘的思路很实用,clear 重置状态也比较干净。 不过看代码片段有两个地方可以再核对下:undo 重绘里似乎拿 undopath[0] 的样式套到所有笔画上,循环里也用了 undopath.x/y,实际应该按每笔分组取点并恢复各自样式,否则多笔不同线宽颜色会不对;另外鼠标坐标做了 offsetX/2、offsetY/2,触摸用 pageX/pageY 没有做画布相对坐标和缩放换算,触摸端可能会有偏移,建议统一成相对 canvas 的坐标再除以 2。构造函数里传入已有 canvas 时还会 appendChild 到 body,这点使用时也要留意。 整体作为在线签字的基础实现已经很够用了,导出图片部分正文还没展开,期待补上 toDataURL 相关处理。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-30 04:45 , Processed in 0.027701 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部