百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

一文解读前端实现电子签名

zhezhongyun 2025-01-17 14:31 86 浏览

大家好,我是Echa。

创作不易,喜欢的老铁们加个关注,点个赞,后面会持续更新干货,速速收藏,谢谢!

在现在的时代发展中,从以前的手写签名,逐渐衍生出了电子签名。电子签名和纸质手写签名一样具有法律效应。电子签名目前主要还是在需要个人确认的产品环节和司法类相关的产品上较多。

举个常用的例子,大家都用过钉钉,钉钉上面就有电子签名,相信大家这肯定是知道的。

那作为前端的我们如何实现电子签名呢?其实在html5中已经出现了一个重要级别的辅助标签,是啥呢?那就是canvas。下面我给大家分享分享几个关于前端如何实现电子签名经典案例以及实现方法。

什么是canvas

Canvas(画布)是在HTML5中新增的标签用于在网页实时生成图像,并且可以操作图像内容,基本上它是一个可以用JavaScript操作的位图(bitmap)。Canvas 对象表示一个 HTML 画布元素 -。它没有自己的行为,但是定义了一个 API 支持脚本化客户端绘图操作。

大白话就是canvas是一个可以在上面通过javaScript画图的标签,通过其提供的context(上下文)及Api进行绘制,在这个过程中canvas充当画布的角色。

实现电子签名

知道几何的朋友都很清楚,线由点绘成,面由线绘成。

多点成线,多线成面。

所以我们实际只需要拿到当前触摸的坐标点,进行成线处理就可以了。

全文大纲

  • vue-sign-canvas 一个基于canvas开发,封装于Vue组件的通用手写签名板
  • Signature Pad 是一个用于绘制平滑签名的JavaScript库。
  • 纯JavaScript实现电子签名,同时支持Web端和移动端。

vue-sign-canvas

在线预览:https://langyuxiansheng.github.io/vue-sign-canvas/

Github:https://github.com/langyuxiansheng/vue-sign-canvas

vue-sign-canvas 一个基于canvas开发,封装于Vue组件的通用手写签名板(电子签名板),支持pc端和移动端,属性支持自定义配置

组件模板使用

<template>
    <div id="app">
        <h2 class="title">Vue Sign Canvas 电子签名板</h2>
        <sign-canvas class="sign-canvas" ref="SignCanvas" :options="options" v-model="value" />
        <img v-if="value" class="view-image" :src="value" width="150" height="150" />
        <div class="config">
            <ul class="ul-config">
                <li class="li-c">
                    <span class="item-label">书写速度:</span>
                    <span class="item-content">
                        <select name="isSign" v-model="options.isSign">
                            <option :value="true">签名</option>
                            <option :value="false">写字</option>
                        </select>
                    </span>
                </li>
                <li class="li-c">
                    <span class="item-label">显示边框/网格:</span>
                    <span class="item-content">
                        <select name="isSign" v-model="options.isShowBorder">
                            <option :value="true">显示</option>
                            <option :value="false">不显示</option>
                        </select>
                    </span>
                </li>
                <li class="li-c">
                    <span class="item-label">兼容高倍屏高清绘制:</span>
                    <span class="item-content">
                        <select name="isSign" v-model="options.isDpr">
                            <option :value="true">启用</option>
                            <option :value="false">关闭</option>
                        </select>
                    </span>
                </li>
                <li class="li-c">
                    <span class="item-label">边框宽度:</span>
                    <span class="item-content">
                        <input v-model="options.borderWidth" type="number" />
                    </span>
                </li>
                <li class="li-c">
                    <span class="item-label">下笔宽度:</span>
                    <span class="item-content">
                        <input v-model="options.writeWidth" type="number" />
                    </span>
                </li>
                <li class="li-c">
                    <span class="item-label">图片类型:</span>
                    <span class="item-content">
                        <input v-model="options.imgType" type="text" />
                    </span>
                </li>
                <li class="li-c">
                    <span class="item-label">线条的边缘类型:</span>
                    <span class="item-content">
                        <select name="lineCap" v-model="options.lineCap">
                            <option value="butt">平直的边缘</option>
                            <option value="round">圆形线帽</option>
                            <option value="square">正方形线帽</option>
                        </select>
                    </span>
                </li>
                <li class="li-c">
                    <span class="item-label">线条交汇时边角的类型:</span>
                    <span class="item-content">
                        <select name="lineCap" v-model="options.lineJoin">
                            <option value="bevel">创建斜角</option>
                            <option value="round">创建圆角</option>
                            <option value="miter">创建尖角</option>
                        </select>
                    </span>
                </li>
                <li class="li-c">
                    <span class="item-label">画笔颜色:</span>
                    <span class="item-content">
                        <input type="color" v-model="options.writeColor" />
                    </span>
                </li>
                <li class="li-c">
                    <span class="item-label">背景色:</span>
                    <span class="item-content">
                        <input type="color" v-model="options.bgColor" />
                    </span>
                </li>
            </ul>
        </div>
        <div class="sign-btns">
            <span id="clear" @click="canvasClear()">清空</span>
            <span id="save" @click="saveAsImg()">保存</span>
            <span id="save" @click="downloadSignImg()">下载</span>
        </div>
    </div>
</template>
<script>
    import SignCanvas from "../packages";
    export default {
        components: { SignCanvas },
        data() {
            return {
                value: null,
                options: {
                    isDpr: false, //是否使用dpr兼容高倍屏 [Boolean] 可选
                    lastWriteSpeed: 1, //书写速度 [Number] 可选
                    lastWriteWidth: 2, //下笔的宽度 [Number] 可选
                    lineCap: "round", //线条的边缘类型 [butt]平直的边缘 [round]圆形线帽 [square]	正方形线帽
                    lineJoin: "bevel", //线条交汇时边角的类型  [bevel]创建斜角 [round]创建圆角 [miter]创建尖角。
                    canvasWidth: 350, //canvas宽高 [Number] 可选
                    canvasHeight: 370, //高度  [Number] 可选
                    isShowBorder: true, //是否显示边框 [可选]
                    bgColor: "#fcc", //背景色 [String] 可选
                    borderWidth: 1, // 网格线宽度  [Number] 可选
                    borderColor: "#ff787f", //网格颜色  [String] 可选
                    writeWidth: 5, //基础轨迹宽度  [Number] 可选
                    maxWriteWidth: 30, // 写字模式最大线宽  [Number] 可选
                    minWriteWidth: 5, // 写字模式最小线宽  [Number] 可选
                    writeColor: "#101010", // 轨迹颜色  [String] 可选
                    isSign: true, //签名模式 [Boolean] 默认为非签名模式,有线框, 当设置为true的时候没有任何线框
                    imgType: "png", //下载的图片格式  [String] 可选为 jpeg  canvas本是透明背景的
                },
            };
        },
        methods: {
            /**
             * 清除画板
             */
            canvasClear() {
                this.$refs.SignCanvas.canvasClear();
            },

            /**
             * 保存图片
             */
            saveAsImg() {
                const img = this.$refs.SignCanvas.saveAsImg();
                alert(`image 的base64:${img}`);
            },

            /**
             * 下载图片
             */
            downloadSignImg() {
                this.$refs.SignCanvas.downloadSignImg();
            },
        },
    };
</script>
<style lang="less">
    * {
        margin: 0;
        padding: 0;
    }
    .title {
        padding: 20px;
        text-align: center;
    }
    .sign-canvas {
        display: block;
        margin: 20px auto;
    }
    .view-image {
        display: block;
        margin: 20px auto;
    }
    .config {
        width: 350px;
        margin: 20px auto;
        .ul-config {
            .li-c {
                display: flex;
                align-items: center;
                padding: 4px 10px;
                .item-label {
                    font-size: 14px;
                }
                .item-content {
                    margin-left: 10px;
                }
            }
        }
    }
    .sign-btns {
        display: flex;
        justify-content: space-between;
        #clear,
        #clear1,
        #save {
            display: inline-block;
            padding: 5px 10px;
            width: 76px;
            height: 40px;
            line-height: 40px;
            border: 1px solid #eee;
            background: #e1e1e1;
            border-radius: 10px;
            text-align: center;
            margin: 20px auto;
            cursor: pointer;
        }
    }
</style>

横屏全屏模式下签名要怎么显示?

<div class="user-sign">
    <template v-if="sign">
        <img class="sign-image" :src="sign" alt="" srcset="" />
    </template>
</div>

<script>
    //局部注册
    import SignCanvas from "sign-canvas";
    import util from "@util";
    import { saveSignature } from "@/http";
    export default {
        name: "UserSign",
        components: { SignCanvas },
        data() {
            return {
                sign: null,
            };
        },
    };
</script>
<style lang="scss" scoped>
    .user-sign {
        background: #e7e7e7;
        height: 9.375rem;
        position: relative;

        .sign-image {
            margin: 0 auto;
            z-index: 9;
            height: 100%;
            transform: rotate(-90deg) scale(1.5);
            display: block;
        }
    }
</style>

如下图:




Signature Pad

在线预览:http://szimek.github.io/signature_pad/

Github:https://github.com/szimek/signature_pad

Signature Pad是一个用于绘制平滑签名的JavaScript库。它基于HTML5画布,使用基于Square发布的Smoother Signatures的可变宽度Bézier曲线插值。它适用于所有现代桌面和移动浏览器,不依赖任何外部库。

核心代码:

const canvas = document.querySelector("canvas");

const signaturePad = new SignaturePad(canvas);

// Returns signature image as data URL (see https://mdn.io/todataurl for the list of possible parameters)
signaturePad.toDataURL(); // save image as PNG
signaturePad.toDataURL("image/jpeg"); // save image as JPEG
signaturePad.toDataURL("image/jpeg", 0.5); // save image as JPEG with 0.5 image quality
signaturePad.toDataURL("image/svg+xml"); // save image as SVG data url

// Return svg string without converting to base64
signaturePad.toSVG(); // "<svg...</svg>"
signaturePad.toSVG({includeBackgroundColor: true}); // add background color to svg output

// Draws signature image from data URL (mostly uses https://mdn.io/drawImage under-the-hood)
// NOTE: This method does not populate internal data structure that represents drawn signature. Thus, after using #fromDataURL, #toData won't work properly.
signaturePad.fromDataURL("data:image/png;base64,iVBORw0K...");

// Draws signature image from data URL and alters it with the given options
signaturePad.fromDataURL("data:image/png;base64,iVBORw0K...", { ratio: 1, width: 400, height: 200, xOffset: 100, yOffset: 50 });

// Returns signature image as an array of point groups
const data = signaturePad.toData();

// Draws signature image from an array of point groups
signaturePad.fromData(data);

// Draws signature image from an array of point groups, without clearing your existing image (clear defaults to true if not provided)
signaturePad.fromData(data, { clear: false });

// Clears the canvas
signaturePad.clear();

// Returns true if canvas is empty, otherwise returns false
signaturePad.isEmpty();

// Unbinds all event handlers
signaturePad.off();

// Rebinds all event handlers
signaturePad.on();

如下图:


纯JavaScript实现电子签名

完整版:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
    </style>
</head>
<body>
    <canvas></canvas>
    <div>
        <button onclick="cancel()">取消</button>
        <button onclick="save()">保存</button>
    </div>
</body>
<script>
    // 配置内容
    const config = {
        width: 400, // 宽度
        height: 200, // 高度
        lineWidth: 5, // 线宽
        strokeStyle: 'red', // 线条颜色
        lineCap: 'round', // 设置线条两端圆角
        lineJoin: 'round', // 线条交汇处圆角
    }

    // 获取canvas 实例
    const canvas = document.querySelector('canvas')
    // 设置宽高
    canvas.width = config.width
    canvas.height = config.height
    // 设置一个边框
    canvas.style.border = '1px solid #000'
    // 创建上下文
    const ctx = canvas.getContext('2d')

    // 设置填充背景色
    ctx.fillStyle = 'transparent'
    // 绘制填充矩形
    ctx.fillRect(
        0, // x 轴起始绘制位置
        0, // y 轴起始绘制位置
        config.width, // 宽度
        config.height // 高度
    );

    // 保存上次绘制的 坐标及偏移量
    const client = {
        offsetX: 0, // 偏移量
        offsetY: 0,
        endX: 0, // 坐标
        endY: 0
    }

    // 判断是否为移动端
    const mobileStatus = (/Mobile|Android|iPhone/i.test(navigator.userAgent))

    // 初始化
    const init = event => {
        // 获取偏移量及坐标
        const { offsetX, offsetY, pageX, pageY } = mobileStatus ? event.changedTouches[0] : event 

        // 修改上次的偏移量及坐标
        client.offsetX = offsetX
        client.offsetY = offsetY
        client.endX = pageX
        client.endY = pageY

        // 清除以上一次 beginPath 之后的所有路径,进行绘制
        ctx.beginPath()
        // 根据配置文件设置相应配置
        ctx.lineWidth = config.lineWidth
        ctx.strokeStyle = config.strokeStyle
        ctx.lineCap = config.lineCap
        ctx.lineJoin = config.lineJoin
        // 设置画线起始点位
        ctx.moveTo(client.endX, client.endY)
        // 监听 鼠标移动或手势移动
        window.addEventListener(mobileStatus ? "touchmove" : "mousemove", draw)
    }
    // 绘制
    const draw = event => {
        // 获取当前坐标点位
        const { pageX, pageY } = mobileStatus ? event.changedTouches[0] : event
        // 修改最后一次绘制的坐标点
        client.endX = pageX
        client.endY = pageY

        // 根据坐标点位移动添加线条
        ctx.lineTo(pageX , pageY )

        // 绘制
        ctx.stroke()
    }
    // 结束绘制
    const cloaseDraw = () => {
        // 结束绘制
        ctx.closePath()
        // 移除鼠标移动或手势移动监听器
        window.removeEventListener("mousemove", draw)
    }
    // 创建鼠标/手势按下监听器
    window.addEventListener(mobileStatus ? "touchstart" : "mousedown", init)
    // 创建鼠标/手势 弹起/离开 监听器
    window.addEventListener(mobileStatus ? "touchend" :"mouseup", cloaseDraw)
    
    // 取消-清空画布
    const cancel = () => {
        // 清空当前画布上的所有绘制内容
        ctx.clearRect(0, 0, config.width, config.height)
    }
    // 保存-将画布内容保存为图片
    const save = () => {
        // 将canvas上的内容转成blob流
        canvas.toBlob(blob => {
            // 获取当前时间并转成字符串,用来当做文件名
            const date = Date.now().toString()
            // 创建一个 a 标签
            const a = document.createElement('a')
            // 设置 a 标签的下载文件名
            a.download = `${date}.png`
            // 设置 a 标签的跳转路径为 文件流地址
            a.href = URL.createObjectURL(blob)
            // 手动触发 a 标签的点击事件
            a.click()
            // 移除 a 标签
            a.remove()
        })
    }
</script>
</html>

各内核和浏览器支持情况

Mozilla 程序从 Gecko 1.8 (Firefox 1.5 (en-US)) 开始支持 <canvas>。它首先是由 Apple 引入的,用于 OS X Dashboard 和 Safari。Internet Explorer 从 IE9 开始支持<canvas> ,更旧版本的 IE 中,页面可以通过引入 Google 的 Explorer Canvas 项目中的脚本来获得<canvas>支持。Google Chrome 和 Opera 9+ 也支持 <canvas>。

小程序中提示

在小程序中我们如果需呀实现的话,也是同样的原理哦,只是我们需要将创建实例和上下文的Api进行修改,因为小程序中是没有dom,既然没有dom,哪来的操作dom这个操作呢。

  • 如果是uni-app则需要使用uni.createCanvasContext进行上下文创建
  • 如果是原生微信小程序则使用wx.createCanvasContext进行创建(2.9.0)之后的库不支持

相关推荐

3 分钟!AI 从零开发五子棋全过程曝光,网友:这效率我服了

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8...

一行代码实现display&quot;过渡动画&quot;原理

作者:Peter谭老师转发链接:https://mp.weixin.qq.com/s/XhwPOv62gypzq5MhhP-5vg写本文的起因上篇文章,提到如何让display出现过渡动画,却没有仔...

脑洞:琼恩·雪诺、蝙蝠侠和魔形女的灵魂宠物了解一下

AlekseiVinogradovisaRussianfreelancedigitalartistwhoshareshisskillsandtalentwith120k...

浏览器的渲染机制、重绘、重排

1、什么是重排和重绘网页生成过程:HTML被HTML解析器解析成DOM树css则被css解析器解析成CSSOM树结合DOM树和CSSOM树,生成一棵渲染树(RenderTree)生成布局(flo...

托福写作高频考题写作思路&amp;词汇丨考虫独家

科技话题与媒体话题是托福写作的常考话题很多考生对这两类话题里的专有词汇表达也许很不了解所以今天就跟随考虫托福写作老师刘云龙老师一起来学习在这些话题的写作里你可以使用哪些有用的表达。希望大家有收获!记得...

在优麒麟上使用 Electron 开发桌面应用

使用Web标准来创建桌面GUI,上手快、成本低、跨平台、自适应分辨率,这些都是Electron的优势。作者/来源:优麒麟Electron是由Github开发,用HTML、CSS和...

php手把手教你做网站(三十八)jquery 转轮盘抽奖,开盲盒

抽奖和开盲盒性质一样的都是通过ajax读取后台的随机数据。1、转轮盘本来是想直接绘图实现轮盘,但是没有找到怎么填充文字,只好把轮盘弄成了背景图,通常用于游戏抽道具,商城积分抽奖,公司年末员工抽奖点击抽...

用 CSS 整活!3D 轮播图手把手教学,快乐代码敲出来

兄弟们,今天咱来搞点好玩的——用CSS整一个3D轮播图!咱野生程序员就是要在代码里找乐子,技术和快乐咱都得要!代码是写不完的,但咱能自己敲出快乐来,走起!一、先整个容器,搭个舞台咋先写一个...

实现一个超酷的 3D 立体卡片效 #前端开发

今天我们来实现一个超酷的3D立体卡片效果。正常情况下就是一个普通的图片展示卡片,鼠标悬停的时候图片会跳出卡片,并将影子投射到背景卡片上,在视觉上有一个3D立体感。html主要分成3个部分:容器→背景层...

Vue 3 Teleport与Suspense:解决UI难题的两个&quot;隐藏大招&quot;

模态框的"层级噩梦"与Teleport的救赎"这个模态框怎么又被父容器截断了?"团队协作开发后台系统时,小张第N次遇到这个问题。多层嵌套的组件结构里,弹窗被overfl...

让交互更加生动!有意思的鼠标跟随 3D 旋转动效

今天,群友问了这样一个问题,如下所示的鼠标跟随交互效果,如何实现:简单分析一下,这个交互效果主要有两个核心:借助了CSS3D的能力元素的旋转需要和鼠标的移动相结合本文,就将讲述如何使用纯CSS...

填坑:transform元素导致zindex失效终极方法

今天遇到了使用css3动画的元素层级被放大置顶的问题,ios浏览器上没问题,安卓原生浏览器和安卓微信上有问题。使用了css3动画的元素z-index失效,兄弟元素设置多高的z-index都盖不住解决办...

诡异的层级错乱:一个被transform隐藏的CSS陷阱

周五下午三点十七分,设计部突然发来紧急截图——原本应该悬浮在顶部的导航菜单,此刻正诡异地被下方的轮播图遮挡。我盯着屏幕上错乱的层级关系,手指下意识地敲下z-index:9999,心里清楚这不过是程序...

动画篇--碎片动画

本文授权转载,作者:Sindri的小巢(简书)前言从最开始动笔动画篇的博客,至今已经过去了四个多月。这段时间回头看了看自己之前的动画文章,发现用来讲解动画的例子确实不那么的赏心悦目。于是这段时间总是想...

Nature:大洋转换断层处的拉张构造与两阶段地壳增生

Nature:大洋转换断层处的拉张构造与两阶段地壳增生转换断层是三种基本的板块边界之一,全球总长度超过48000km(Bird,2003),它们的发现为板块构造理论的建立奠定了重要的基础(Wil...