JS案例:支持PC端和Mobile端的Canvas电子签名功能

本文最后更新于:9 个月前

前言:
这段时间项目迭代时遇到了一个新需求,基于react实现一个Pc版电子签名功能,并生成图片上传。于是我想到了signature_pad,并且在项目使用了这个插件
不得不说,用别人造的轮子是真的香,出于好奇,想用原生实现一下电子签名的功能

以下是实现过程

HTML和css可以参照源码,这里不过多介绍

首先引入eventBus,方便代码解耦
然后实现Base基类存放公共方法和属性,后续有啥共用属性或方法可以往这加

//基类:公共方法和属性
import event from './eventBus.js'
export default class Base {
    constructor(canvasEle, dom = document) {
        this.event = event //注册发布订阅
        this.canvasEle = canvasEle //待操作的画布标签
        this.dom = dom //dom
        return this;
    }
}

完成之后,我们先实现Pc版的电子签名功能,新建一个PcPrint继承自Base,参照之前写的鼠标拖拽案例,实现在canvas上拖拽功能,并将事件结果的坐标发布出去。
其中clearDefaultEvent函数和getClient函数在Base类中实现

// PC端,鼠标事件
import Base from './base.js'
let that = null
export default class PcPrint extends Base {
    constructor(ele, dom) {
        super(ele, dom)
        that = this //注册全局this
        this.init()
        return this;
    }
    init() {
        that.canvasEle.addEventListener('mousedown', that.onMouseDown)
    }
    onMouseDown(e = event) {
        that.clearDefaultEvent(e)
        that.dom.addEventListener('mouseup', that.onMouseUp) //给dom添加mouseup避免产生鼠标点下时,移出画布造成其他的问题
        that.canvasEle.addEventListener('mousemove', that.onMouseMove)
        that.event.emitEvent('pointStart', that.getClient(e)) //触发开始签字事件
    }
    onMouseUp(e = event) {
        that.clearDefaultEvent(e)
        that.event.emitEvent('pointEnd') //触发结束签字事件
        that.canvasEle.removeEventListener('mousemove', that.onMouseMove) //移除移动事件
    }
    onMouseMove(e = event) {
        that.clearDefaultEvent(e)
        that.event.emitEvent('pointMove', that.getClient(e)) //触发签字事件
    }

}

Base类添加以下代码:

/**
 * 取消默认事件和事件冒泡
 * @param e 事件对象
 */
clearDefaultEvent(e) {
    e.preventDefault()
    e.stopPropagation()
}
/**
 * 获取事件元素离body可视区域的坐标
 * @param target 事件目标
 */
getClient(target) {
    return {
        x: target.clientX,
        y: target.clientY
    }
}

接着,我们对事件抛出的三个发布进行订阅,新建Print类,对获取的坐标通过canvas进行绘制

import Base from "./Base.js"
import PcPrint from './pc.js';
// import MobilePrint from './mobile.js';
let that = null
export default class Print extends Base {
    constructor(canvasEle, options, dom) {
        super(canvasEle, dom)
        that = this
        this.options = options //配置画笔颜色,粗细,是否开启移动端或PC端,
        this.init() //初始化属性,配置,注册发布订阅等
        this.initCanvas() //初始化画布
        return this
    }
    init() {
        //Pc和Mobile启用开关
        this.Pc = this.options.Pc ? (new PcPrint(this.canvasEle)) : null
        // this.Mobile = this.options.Mobile ? (new MobilePrint(this.canvasEle)) : null
        this.point = null //存储上一次坐标
        this.event.onEvent('pointMove', that.pointMove) //订阅签字事件
        this.event.onEvent('pointStart', that.pointStart) //订阅签字开始事件
        this.event.onEvent('pointEnd', that.pointEnd) //订阅签字结束事件
    }
    initCanvas() {
        this.clientRect = this.canvasEle.getBoundingClientRect() // 获取标签相对可视区域的偏移量
        this.canvasEle.width = this.canvasEle.parentNode.offsetWidth //设置为父元素的宽
        this.canvasEle.height = this.canvasEle.parentNode.offsetHeight //设置为父元素的高
        this.context = this.canvasEle.getContext('2d')
        this.context.strokeStyle = this.options.color; // 线条颜色
        this.context.lineWidth = this.options.weight; // 线条宽度
    }
    pointStart(point) {
        that.point = that.shiftingPosition(point, that.clientRect) //初始化起始位置
    }
    pointEnd() {
        that.point = null //清空起始位置
    }
    pointMove(point) {
        that.canvasDraw(that.shiftingPosition(point, that.clientRect)) //签字效果
    }
    canvasDraw(point) { //画布操作
        this.context.beginPath() //新建(重置)路径
        this.context.moveTo(this.point.x, this.point.y) //画布绘画起始点移动到前一个坐标
        this.context.lineTo(point.x, point.y) //画布从前一个坐标到当前坐标
        this.context.stroke() //从moveTo到lineTo进行绘制
        this.context.closePath() //创建从当前坐标回到前一个坐标的路径
        that.point = point //将此次坐标赋值给下一次移动时的前一个坐标
    }
}

考虑到canvas的偏移问题,在Base中添加shiftingPosition函数,解决画布绘制时坐标偏移问题

/**
  * 抵消画布偏移
  * @param point 当前坐标
  * @param shift 偏移量
  */
 shiftingPosition(point, shift) {
     return {
         x: point.x - shift.left,
         y: point.y - shift.top
     }
 }

最后,在index中实例化电子签名

<script type="module">
    import Print from "./js/print.js"
    new Print(printBox,{
        Pc:true,
        Mobile:true,
        color:'lightcoral',
        weight:5
    })
</script>

效果如下:

Pc端实现完成之后是Mobile端,代码大同小异,除了事件类型不用之外,还一点就是移动端的多指触碰支持,touchevent支持双指事件,此时我们要判断是否单指输入

// Mobile端,触摸事件
import Base from './base.js'
let that = null
export default class MobilePrint extends Base {
    constructor(ele, dom) {
        super(ele, dom)
        that = this //注册全局this
        this.init()
        return this;
    }
    init() {
        that.canvasEle.addEventListener('touchstart', that.onTouchStart)
    }
    onTouchStart(e = event) {
        that.clearDefaultEvent(e)
        that.canvasEle.addEventListener('touchend', that.onTouchEnd) //没有像pc一样给dom添加touchend,因为touchmove是基于touchstart和touchend之间触发的,只要touchend触发,touchmove便失效
        that.canvasEle.addEventListener('touchmove', that.onTouchMove)
        that.event.emitEvent('pointStart', that.getClient(e.touches[0])) //这里可以做一个判断e.touches是否只有一个(e.touches表示有几个手指触碰)
    }
    onTouchEnd(e = event) {
        that.clearDefaultEvent(e)
        that.event.emitEvent('pointEnd')
        that.canvasEle.removeEventListener('touchmove', that.onTouchMove)
    }
    onTouchMove(e = event) {
        that.clearDefaultEvent(e)
        that.event.emitEvent('pointMove', that.getClient(e.touches[0]))
    }
}

在移动端实现的效果:

最后:
附上源码地址:Gitee

感谢你的阅读,如果这篇文章对你有帮助,希望三连支持一下,你的支持是我创作的动力,同时也欢迎大佬建议指正