一文解讀前端實現電子簽名

echa攻城獅 發佈 2022-12-23T21:17:46.247977+00:00

大家好,我是Echa。創作不易,喜歡的老鐵們加個關注,點個讚,後面會持續更新乾貨,速速收藏,謝謝!在現在的時代發展中,從以前的手寫簽名,逐漸衍生出了電子簽名。電子簽名和紙質手寫簽名一樣具有法律效應。電子簽名目前主要還是在需要個人確認的產品環節和司法類相關的產品上較多。

大家好,我是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)之後的庫不支持
關鍵字: