2025年前端手写电子签名板实现方案

前端手写电子签名板实现方案前端手写电子签名板实现 作者 很菜的小白在分享 时间 2022 年 12 月 29 日 介绍 什么是电子签名 电子签名是指数据电文中以电子形式所含 所附用于识别签名人身份并表明签名人认可其中内容的数据 百度百科 通俗点说其实就是通过在电子设备上进行类似纸面上签字的效果

大家好,我是讯享网,很高兴认识大家。

前端手写电子签名板实现



示例图
讯享网

介绍

什么是电子签名

电子签名是指数据电文中以电子形式所含、所附用于识别签名人身份并表明签名人认可其中内容的数据。—— 百度百科

通俗点说其实就是通过在电子设备上进行类似纸面上签字的效果。在如今互联网快速发展的时代,可以实现云签名,在线签订合同等各种场景。


初衷

这个功能是我在日常开发项目时遇到的需求场景,背景是公司开发的在线教育客户端,允许学生在客户端进行学习和答题,题目类型中有一项“解答题”,需要学生通过手写答题的方式进行作答。因为是第一次接触在线教育类型项目和这样的需求,所以产生了一些思考。

由于项目是使用uniApp开发的Pad客户端,在实现上与原生js有所区别,所以本文将通过原生js的方式重新实现一遍,并且让组件更加灵活通用。


思考

在刚接触到这个需求时,有的同学表示懵逼3连,这怎么实现?其实你只要相信,只要产品能提出的需求,世面上存在这样的功能,不管是用什么语言实现的,排除一些特殊功能,js大部分都可以实现。<canvas> 标签会在页面上创建一块画布允许我们在上面绘制各种图形,下面介绍实现该功能的主要技术点,如果对 Canvas 技术了解的可以直接跳过介绍部分。


什么是 canvas

Canvas API 提供了一个通过JavaScript 和 HTML的<canvas>元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。 —— MDN

举个例子:比如我们要实现一个长、宽各为50px的正方形。

正常操作:

 <div style="width: 50px; height: 50px; background-color: #000;"></div> 

讯享网

Canvas方式:

讯享网 <canvas id="myCanvas" width="200px" height="200px"></canvas> <script> const canvas = document.getElementById('canvas') const ctx = canvas.getContext("2d") ctx.fillStyle = "#000" ctx.fillRect(0, 0, 50, 50) </script> 

实现效果

虽然这个例子中 Canvas 的实现方式更为复杂,那是因为 Canvas 在某些效果方面确实优于 html + css的方式,比如你需要绘制 sinx 的曲线,使用html+css方式就没有那么容易了,相反 Canvas可以轻松实现。

下面介绍一些常用到的 Canvas API。


Canvas 常用API
属性名 说明 返回值
width <canvas>元素的width属性,以CSS像素表示,未指定或者值为无效值(例如负数),则默认为300 <canvas>元素的宽度
height <canvas>元素的height属性,以CSS像素表示,未指定或者值为无效值(例如负数),则默认为150 <canvas>元素的高度
Canvas 常用方法
方法名 说明 参数 返回值
captureStream() 返回 CanvasCaptureMediaStream ,它是对画布表面的实时视频捕获。 CanvasCaptureMediaStream
getContext() 返回画布上的绘图上下文;如果不支持上下文 ID,则返回 null。 2d webgl webgl2 bitmaprenderer CanvasContext
toDataURL() 返回由类型参数指定的格式的图像数据URL。参考 type encoderOptions Data Url
toBlob() 创建一个Blob 对象,表示 canvas 中包含的图像;该文件可以由用户代理决定是否缓存在磁盘上或存储在内存中。参考 callback type type quality
CanvasRenderingContext2D 常用API
属性名 说明
canvas 获取上下文关联的Canvas元素
fillStyle 描述颜色和样式的属性。默认值是 #000 (黑色)。
font 描述绘制文字时,当前字体样式的属性。
lineCap 指定如何绘制每一条线段末端的属性。有 3 个可能的值,分别是:butt, round and square。默认值是 butt。
lineWidth 设置线段厚度的属性(即线段的宽度)。
strokeStyle 描述画笔(绘制图形)颜色或者样式的属性。默认值是 #000 (black)。
textAlign 描述绘制文本时,文本的对齐方式的属性。
textAlign 描述绘制文本时,文本的对齐方式的属性。
CanvasRenderingContext2D 常用方法

参考

方法名 说明
arc() 绘制圆弧路径的方法。
beginPath() 通过清空子路径列表开始一个新路径的方法。当你想创建一个新的路径时,调用此方法。
clearRect() 通过把像素设置为透明以达到擦除一个矩形区域的目的。
drawImage() 提供了多种在画布(Canvas)上绘制图像的方式。
getImageData() 返回一个ImageData对象,用来描述 canvas 区域隐含的像素数据,这个区域通过矩形表示,起始点为(sx, sy)、宽为sw、高为sh。
lineTo() 使用直线连接子路径的终点到 x,y 坐标的方法(并不会真正地绘制)。
moveTo() 将一个新的子路径的起始点移动到 (x,y) 坐标的方法。
measureText() 返回一个关于被测量文本TextMetrics 对象包含的信息(例如它的宽度)。
save() 通过将当前状态放入栈中,保存 canvas 全部状态的方法。
stroke() 使用非零环绕规则,根据当前的画线样式,绘制当前或已经存在的路径的方法。

由于文章内容有限所以只列举常用 API 及 方法,想了解更多可以前往 MDN 了解更多 Canvas 相关知识。


实现

实现思路

实现思路大致就是,通过当前鼠标移动的位置在 Canvas 画布上绘制出鼠标的移动轨迹点,然后利用 Canvas 提供的 API 将点连成线来实现手写签名的过程。


具体实现

 / * @class * @classdesc 实现手写签名的构造函数 * @param {Object} 手写签名画板的配置项 */ function DrawingBoard(options) { 
    if (!(this instanceof DrawingBoard)) { 
    throw new TypeError("DrawingBoard constructor cannot be invoked without 'new'") } this.options = options this.canvas = null this.ctx = null this.drawStatus = false const _this = this / * @method * @param {Event} event 事件对象 * @desc 鼠标按下事件 */ DrawingBoard.prototype.touchStartPC = function (event) { 
    _this.drawStatus = true const { 
   offsetX, offsetY} = event _this.createBrush(offsetX, offsetY) } / * @method * @param {Event} event 事件对象 * @desc 鼠标移动事件 */ DrawingBoard.prototype.touchMovePC = function (event) { 
    if (!_this.drawStatus) return; const { 
   offsetX, offsetY} = event _this.drawPixel({ 
   offsetX, offsetY}) } / * @method * @param {Event} event 事件对象 * @desc 鼠标抬起事件 */ DrawingBoard.prototype.touchEndPC = function (event) { 
    _this.drawStatus = false } this.initSignatureCanvas() } / * 初始化画板 */ DrawingBoard.prototype.initSignatureCanvas = function () { 
    let canvas = document.createElement('canvas') canvas.setAttribute('width', '200px') canvas.setAttribute('height', '200px') this.canvas = canvas this.bindEvent(canvas) this.ctx = canvas.getContext('2d') this.options.el?.appendChild(canvas) } / * @method * @param {Number} x 画笔的 x 坐标 * @param {Number} y 画笔的 y 坐标 * @desc 移动画笔创建连接点 */ DrawingBoard.prototype.createBrush = function (x, y) { 
    this.ctx.beginPath() this.ctx.moveTo(x, y) } / * @method * @param {Object} options 绘制线条的 x,y 坐标 * @desc 根据点坐标绘制连线 */ DrawingBoard.prototype.drawPixel = function (options) { 
    const { 
   offsetX, offsetY} = options this.ctx.lineTo(offsetX, offsetY) this.ctx.fill() this.ctx.stroke(); this.createBrush(offsetX, offsetY) } / * @method * @desc 事件绑定 */ DrawingBoard.prototype.bindEvent = function () { 
    this.canvas.addEventListener("mousedown", this.touchStartPC) this.canvas.addEventListener("mousemove", this.touchMovePC) this.canvas.addEventListener("mouseup", this.touchEndPC) } 

示例

讯享网 <style> #drawingBoard { 
      position: absolute; top: 200px; left: 200px; width: 200px; border: 2px solid #000; border-radius: 4px; overflow: hidden; } </style> <div id="drawingBoard"></div> 
 var board = DrawingBoard({ 
    el: document.getElementById('drawingBoard'), }) 

效果

实现效果

扩展

自定义画布尺寸

initSignatureCanvas方法调整,新增 addAttribute 为画布添加属性样式。

讯享网 / * 初始化画板 */ DrawingBoard.prototype.initSignatureCanvas = function () { 
    let canvas = document.createElement('canvas') this.canvas = canvas // 新增 this.addAttribute(canvas) this.bindEvent(canvas) this.ctx = canvas.getContext('2d') this.options.el?.appendChild(canvas) } / * @method * @desc 为容器和画布添加属性样式 */ DrawingBoard.prototype.addAttribute = function () { 
    let { 
    width, height, style } = this.options const container = this.getClientRect() let styleParse = '' if(!width) width = '200px' if(!height) height = '200px' for (const key in style) { 
    if (Object.prototype.hasOwnProperty.call(style, key)) { 
    styleParse += `${ 
     toSplitLine(key)}: ${ 
     style[key]};` } } this.canvas.setAttribute('width', width) this.canvas.setAttribute('height', height) this.canvas.setAttribute('style', styleParse) if (this.options.el) { 
    if(container.width <= 0) this.options.el.style.width = width if (container.height <= 0) this.options.el.style.height = height if (this.options.brush && this.options.brush.pointer) { 
    this.options.el.style.cursor = `url(${ 
     this.options.brush.pointer}) 0 16, default` } } } 

options 配置项增加 width height 属性,允许传入画布尺寸信息,例:‘200px’。当未设置 #drawingBoard 容器宽高时,使用 options.width,默认:200px

示例
 var board = new DrawingBoard({ 
    el: document.getElementById('drawingBoard'), width: '200px', height: '200px' }) 

画板的禁用&启用

disable enable 用于切换画板是否启用。

讯享网 / * @method * @desc 禁用画布 */ DrawingBoard.prototype.disable = function () { 
    this.isDisable = true this.unBindEvent() } / * @method * @desc 启用画布 */ DrawingBoard.prototype.enable = function () { 
    this.isDisable = false this.bindEvent() } / * @method * @desc 解除事件绑定 */ DrawingBoard.prototype.unBindEvent = function () { 
    this.canvas.removeEventListener("mousedown", this.touchStartPC) this.canvas.removeEventListener("mousemove", this.touchMovePC) this.canvas.removeEventListener("mouseup", this.touchEndPC) } 
示例
 <div id="drawingBoard"></div> <button id="disableBtn">禁用</button> <button id="enableBtn">启用</button> 
讯享网 var board = new DrawingBoard({ 
    el: document.getElementById('drawingBoard'), }) disableBtn.onclick = disabled enableBtn.onclick = enabled function disabled() { 
    board.disable() } function enabled() { 
    board.enable() } 

设置画笔样式&画板背景

 / * @method * @param {Event} event 事件对象 * @desc 鼠标按下事件 */ DrawingBoard.prototype.touchStartPC = function (event) { 
    _this.drawStatus = true const { 
    offsetX, offsetY } = event // 新增 if (_this.isRubber) { 
    _this.eraseBoard(offsetX, offsetY) return } _this.createBrush(offsetX, offsetY) } / * @method * @param {Event} event 事件对象 * @desc 鼠标移动事件 */ DrawingBoard.prototype.touchMovePC = function (event) { 
    if (!_this.drawStatus) return; const { 
    offsetX, offsetY } = event // 新增 if (_this.isRubber) { 
    _this.eraseBoard(offsetX, offsetY) return } _this.drawPixel({ 
   offsetX, offsetY}) } / * 初始化画板 */ DrawingBoard.prototype.initSignatureCanvas = function () { 
    let canvas = document.createElement('canvas') this.canvas = canvas this.addAttribute(canvas) this.bindEvent(canvas) this.ctx = canvas.getContext('2d') this.options.el?.appendChild(canvas) // 新增 this.ctx.fillStyle = this.options.background || '#fff' this.ctx.fillRect(0, 0, canvas.width, canvas.height) } / * @method * @param {Object} options 绘制线条的 x,y 坐标 * @desc 根据点坐标绘制连线 */ DrawingBoard.prototype.drawPixel = function (options) { 
    const { 
   offsetX, offsetY} = options // 新增 this.ctx.strokeStyle = this.options.brush.color || '#000' this.ctx.lineWidth = this.options.brush.lineWidth || 2 // 设置绘制线段末端结束的形式 this.ctx.lineCap = 'round' this.ctx.lineJoin = 'round' this.ctx.lineTo(offsetX, offsetY) this.ctx.fill() this.ctx.stroke(); this.createBrush(offsetX, offsetY) } / * @method * @param {Number} x 画笔的 x 坐标 * @param {Number} y 画笔的 y 坐标 * @desc 擦除画布 */ DrawingBoard.prototype.eraseBoard = function (x, y) { 
    this.ctx.clearRect(x, y, this.options.brush.lineWidth, this.options.brush.lineWidth) } / * @mehtod * @desc 切换画笔和橡皮模式 */ DrawingBoard.prototype.switchBrush = function () { 
    if (this.isRubber) { 
    this.isRubber = false this.options.el.style.cursor = `url(${ 
     this.options.brush.pointer}) 0 16, default` } else { 
    this.isRubber = true this.options.el.style.cursor = `url('./assets/rubber.png') 0 16, default` } } / * @mehtod * @param {Object} brush 画笔样式配置项 * @desc 设置画笔样式 */ DrawingBoard.prototype.setBrush = function (brush) { 
    Object.assign(this.options.brush, brush) } 

options 配置项增加 background brush 属性,允许传入画布背景与画笔配置信息。brush 属性包含 画笔颜色:color,画笔粗细:lineWidth,画笔icon:pointer(目前仅支持设置PC端鼠标指针)。

示例
讯享网 <div id="drawingBoard"></div> <div> <label for="colorSelect">画笔颜色:</label> <input type="color" id="colorSelect"> </div> <div> <label for="numberSelect">画笔粗细:</label> <input type="number" id="numberSelect"> </div> 
 var board = new DrawingBoard({ 
    el: document.getElementById('drawingBoard'), background: '#000', brush: { 
    color: '#fff', lineWidth: 10, pointer: './assets/brush.png' } }) colorSelect.onchange = colorSelectChange numberSelect.onchange = numberSelectChange function colorSelectChange (event) { 
    board.setBrush({ 
    color: event.target.value }) } function numberSelectChange(event) { 
    board.setBrush({ 
    lineWidth: event.target.value }) } 

导出画布信息为图片资源

讯享网 / * @method * @param {Function} cb 通过回调函数返回画布信息 * @desc 保存画布信息为图片资源 */ DrawingBoard.prototype.save = function (cb) { 
    if (this.options.exportType === 'blob') { 
    this.canvas.toBlob(function (blob) { 
    const blobUrl = URL.createObjectURL(blob) cb(blobUrl) }, this.options.mimeType) } else { 
    const base64 = this.canvas.toDataURL(this.options.mimeType) cb(base64) } } 

options 配置项增加 mimeType exportType 属性,mimeType 设置导出图片资源格式,默认 image/pngexportType 设置导出资源的数据形式,例如:base64 or blob,默认:base64,目前仅支持PC端,暂不支持移动端导出。

完整代码

文档

 / * @method * @param {String} str 需要转换的字符串 * @returns 转换后的字符串 * @desc 将驼峰命名转化为使用分隔符的字符串 */ function toSplitLine(str) { 
    var reg = /[A-Z]/g; var newStr = str.replace(reg, function ($0) { 
    return '-' + $0.toLocaleLowerCase(); }); if (newStr.substring(0, 1) === '-') { 
    newStr = newStr.substring(1); } return newStr; } / * @class * @classdesc 实现手写签名的构造函数 * @param {Object} 手写签名画板的配置项 */ function DrawingBoard(options) { 
    if (!(this instanceof DrawingBoard)) { 
    throw new TypeError("DrawingBoard constructor cannot be invoked without 'new'") } this.options = options this.canvas = null this.ctx = null this.drawStatus = false this.isDisable = true this.isRubber = false const _this = this / * @method * @param {Event} event 事件对象 * @desc 鼠标按下事件 */ DrawingBoard.prototype.touchStartPC = function (event) { 
    _this.drawStatus = true const { 
    offsetX, offsetY } = event if (_this.isRubber) { 
    _this.eraseBoard(offsetX, offsetY) return } _this.createBrush(offsetX, offsetY) } / * @method * @param {Event} event 事件对象 * @desc 鼠标移动事件 */ DrawingBoard.prototype.touchMovePC = function (event) { 
    if (!_this.drawStatus) return; const { 
    offsetX, offsetY } = event if (_this.isRubber) { 
    _this.eraseBoard(offsetX, offsetY) return } _this.drawPixel({ 
   offsetX, offsetY}) } / * @method * @param {Event} event 事件对象 * @desc 鼠标抬起事件 */ DrawingBoard.prototype.touchEndPC = function (event) { 
    _this.drawStatus = false } this.initSignatureCanvas() } / * 初始化画板 */ DrawingBoard.prototype.initSignatureCanvas = function () { 
    this.isDisable = false let canvas = document.createElement('canvas') this.canvas = canvas this.addAttribute(canvas) this.bindEvent(canvas) this.ctx = canvas.getContext('2d') this.options.el?.appendChild(canvas) this.ctx.fillStyle = this.options.background || '#fff' this.ctx.fillRect(0, 0, canvas.width, canvas.height) } / * @method * @param {Number} x 画笔的 x 坐标 * @param {Number} y 画笔的 y 坐标 * @desc 移动画笔创建连接点 */ DrawingBoard.prototype.createBrush = function (x, y) { 
    this.ctx.beginPath() this.ctx.moveTo(x, y) } / * @method * @param {Object} options 绘制线条的 x,y 坐标 * @desc 根据点坐标绘制连线 */ DrawingBoard.prototype.drawPixel = function (options) { 
    const { 
   offsetX, offsetY} = options this.ctx.strokeStyle = this.options.brush.color || '#000' this.ctx.lineWidth = this.options.brush.lineWidth || 2 // 设置绘制线段末端结束的形式 this.ctx.lineCap = 'round' this.ctx.lineJoin = 'round' this.ctx.lineTo(offsetX, offsetY) this.ctx.fill() this.ctx.stroke(); this.createBrush(offsetX, offsetY) } / * @method * @param {Number} x 画笔的 x 坐标 * @param {Number} y 画笔的 y 坐标 * @desc 擦除画布 */ DrawingBoard.prototype.eraseBoard = function (x, y) { 
    this.ctx.clearRect(x, y, this.options.brush.lineWidth, this.options.brush.lineWidth) } / * @mehtod * @desc 切换画笔和橡皮模式 */ DrawingBoard.prototype.switchBrush = function () { 
    if (this.isRubber) { 
    this.isRubber = false this.options.el.style.cursor = `url(${ 
     this.options.brush.pointer}) 0 16, default` } else { 
    this.isRubber = true this.options.el.style.cursor = `url('./assets/rubber.png') 0 16, default` } } / * @mehtod * @param {Object} brush 画笔样式配置项 * @desc 设置画笔样式 */ DrawingBoard.prototype.setBrush = function (brush) { 
    Object.assign(this.options.brush, brush) } / * @method * @param {Function} cb 通过回调函数返回画布信息 * @desc 保存画布信息为图片资源 */ DrawingBoard.prototype.save = function (cb) { 
    if (this.options.exportType === 'blob') { 
    this.canvas.toBlob(function (blob) { 
    const blobUrl = URL.createObjectURL(blob) cb(blobUrl) }, this.options.mimeType) } else { 
    const base64 = this.canvas.toDataURL(this.options.mimeType) cb(base64) } } / * @method * @desc 禁用画布 */ DrawingBoard.prototype.disable = function () { 
    this.isDisable = true this.unBindEvent() } / * @method * @desc 启用画布 */ DrawingBoard.prototype.enable = function () { 
    this.isDisable = false this.bindEvent() } / * @method * @desc 获取父容器宽高 */ DrawingBoard.prototype.getClientRect = function () { 
    return { 
    width: this.options.el.scrollWidth, height: this.options.el.scrollHeight } } / * @method * @desc 为容器和画布添加属性样式 */ DrawingBoard.prototype.addAttribute = function () { 
    let { 
    width, height, style } = this.options const container = this.getClientRect() let styleParse = '' for (const key in style) { 
    if (Object.prototype.hasOwnProperty.call(style, key)) { 
    styleParse += `${ 
     toSplitLine(key)}: ${ 
     style[key]};` } } if(!width) width = '200px' if(!height) height = '200px' this.canvas.setAttribute('width', width) this.canvas.setAttribute('height', height) this.canvas.setAttribute('style', styleParse) if (this.options.el) { 
    if(container.width <= 0) this.options.el.style.width = width if (container.height <= 0) this.options.el.style.height = height if (this.options.brush && this.options.brush.pointer) { 
    this.options.el.style.cursor = `url(${ 
     this.options.brush.pointer}) 0 16, default` } } } / * @method * @desc 事件绑定 */ DrawingBoard.prototype.bindEvent = function () { 
    this.canvas.addEventListener("mousedown", this.touchStartPC) this.canvas.addEventListener("mousemove", this.touchMovePC) this.canvas.addEventListener("mouseup", this.touchEndPC) } / * @method * @desc 解除事件绑定 */ DrawingBoard.prototype.unBindEvent = function () { 
    this.canvas.removeEventListener("mousedown", this.touchStartPC) this.canvas.removeEventListener("mousemove", this.touchMovePC) this.canvas.removeEventListener("mouseup", this.touchEndPC) } 

工具类函数

讯享网 / * @method * @param {String} str 需要转换的字符串 * @returns 转换后的字符串 * @desc 将驼峰命名转化为使用分隔符的字符串 */ function toSplitLine(str) { 
    var reg = /[A-Z]/g; var newStr = str.replace(reg, function ($0) { 
    return '-' + $0.toLocaleLowerCase(); }); if (newStr.substring(0, 1) === '-') { 
    newStr = newStr.substring(1); } return newStr; } 

完结

以上就是整个功能的实现,如果存在缺陷请联系及时改正。
如果本文对你有帮助,记得留下点痕迹,让我知道你来过。
欢迎评论区讨论,共同进步,2022 即将结束,2023继续努力!!

完结

小讯
上一篇 2025-02-25 19:09
下一篇 2025-03-09 11:47

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/20730.html