最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

用fabric.js搞定電子簽名拖拽合成圖片的完整步驟

 更新時(shí)間:2026年05月22日 11:00:48   作者:wayhome在哪  
fabric.js是一個(gè)強(qiáng)大的JavaScript HTML5 canvas庫(kù),它讓開發(fā)者可以輕松操作畫布上的圖形、文本和圖像,支持縮放、旋轉(zhuǎn)、拖拽等交互功能,這篇文章主要介紹了用fabric.js搞定電子簽名拖拽合成圖片的相關(guān)資料,需要的朋友可以參考下

前言

上周組長(zhǎng)甩過來一個(gè)需求:“客戶要在合同圖片上簽電子簽名,還得能拖著簽名調(diào)整位置” 雖然是真的懵逼但也還是擠出一句 “好的,我看看”。查了一圈資料,發(fā)現(xiàn) fabric.js 這玩意兒簡(jiǎn)直是為這種 Canvas 交互量身定做的,空談理論不如上手實(shí)踐,今天就把實(shí)現(xiàn)過程掰開揉碎了講給大家。

Fabric.js Javascript Library

先搭個(gè)基礎(chǔ)架子

任何前端功能實(shí)現(xiàn)的第一步都是搭建基礎(chǔ)環(huán)境。練習(xí)就直接用 CDN 引入 fabric.js了,畢竟不是每個(gè)項(xiàng)目都有閑工夫搞 npm 那套復(fù)雜流程。在 HTML 里放個(gè) Canvas 元素,再配個(gè)簽名板和幾個(gè)按鈕,基本布局就有了:

<script src="https://cdn.jsdelivr.net/npm/fabric@5.3.0/dist/fabric.min.js"></script>
<div class="container">
        <h3 style="padding-bottom: 5px;">合同圖片簽名區(qū)</h3>
        <canvas id="canvas" width="800" height="400"></canvas>
        
        <div class="signature-area">
            <h4>電子簽名</h4>
            <p class="tips">在下方畫布簽名,完成后點(diǎn)擊保存簽名</p>
            <canvas id="signaturePad" width="800" height="150"></canvas>
            
            <!-- 新增簽名操作區(qū) -->
            <div class="signature-actions">
                <button id="clearCurrentSignature">清空當(dāng)前簽名</button>
                <button id="saveSignature">保存簽名到畫布</button>
                <button id="uploadImage">上傳合同圖片</button>
                <input type="file" id="fileInput" accept="image/*" style="display:none">
                <button id="removeSelected">刪除選中簽名</button>
                <button id="clearCanvas">清空畫布</button>
            </div>
        </div>    
    </div>

CSS 部分簡(jiǎn)單美化一下,讓簽名板和主畫布分開顯示,按鈕排整齊點(diǎn),別讓用戶覺得我們的界面太潦草。

初始化 fabric 畫布

這一步是核心中的核心。初始化 fabric.Canvas 實(shí)例時(shí),最好加上 preserveObjectStacking 屬性,不然拖拽元素時(shí)層級(jí)會(huì)亂得讓人頭大。我吃過這方面的虧,大家一定要注意:

const canvas = new fabric.Canvas('canvas', {
  preserveObjectStacking: true,
  backgroundColor: '#fff'
});

初始化后可以先放張默認(rèn)圖片,讓用戶有個(gè)直觀感受。用 fabric.Image.fromURL 方法加載圖片,記得設(shè)置 selectable 為 true,這樣才能拖拽調(diào)整。

實(shí)現(xiàn)電子簽名功能

簽名功能用原生 Canvas 實(shí)現(xiàn)更靈活。給簽名板 Canvas 綁定 mousedown、mousemove 和 mouseup 事件,在 mousemove 時(shí)用 lineTo 繪制線條。這里有個(gè)小技巧,設(shè)置 lineCap 為 round 可以讓簽名更流暢自然。

保存簽名時(shí),把簽名板 Canvas 轉(zhuǎn)換成圖片對(duì)象,然后添加到 fabric 畫布中:

document.getElementById('saveSignature').addEventListener('click', () => {
            // 檢查簽名是否為空
            const imageData = sigCtx.getImageData(0, 0, signaturePad.width, signaturePad.height);
            const data = imageData.data;
            let hasContent = false;
            
            for (let i = 0; i < data.length; i += 4) {
                if (data[i + 3] > 0) {
                    hasContent = true;
                    break;
                }
            }
            
            if (!hasContent) {
                alert('請(qǐng)先完成簽名再保存');
                return;
            }
            
            const signatureImg = new Image();
            signatureImg.src = signaturePad.toDataURL('image/png');
            
            signatureImg.onload = function() {
                const fabricImg = new fabric.Image(signatureImg, {
                    left: 100,
                    top: 100,
                    opacity: 1,
                    // 啟用縮放功能
                    lockScalingX: false,
                    lockScalingY: false,
                    lockUniScaling: false,
                    // 可選設(shè)置
                    minScaleLimit: 0.3, // 最小縮放比例
                    maxScaleLimit: 3,   // 最大縮放比例
                    isSignature: true
                });
                canvas.add(fabricImg);
                canvas.setActiveObject(fabricImg);
                sigCtx.clearRect(0, 0, signaturePad.width, signaturePad.height);
            };
        });

完整代碼

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>電子簽名合成工具</title>
    <!-- 引入fabric.js -->
    <script src="https://cdn.jsdelivr.net/npm/fabric@5.3.0/dist/fabric.min.js"></script>
    <style>
        *{
            margin:0;
            padding:0;
            box-sizing: border-box;
        }
        body{
            display:flex;
            justify-content: center;
            align-items: center;
        }
        .container {
            width: 90%;
            margin: 20px auto;
            padding: 20px;
            border: 1px solid #eee;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            display:flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;
        }
        #canvas {
            border: 1px dashed #ccc;
            background-color: #f9f9f9;
        }
        .signature-area {
            margin: 20px 0;
        }
        #signaturePad {
            border: 1px solid #333;
            background-color: white;
        }
        .signature-actions {
            margin-top: 10px;
            display: flex;
            gap: 10px;
        }
        .btn-group {
            margin-top: 15px;
            display: flex;
            gap: 10px;
        }
        button {
            padding: 8px 16px;
            background-color: #4285f4;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background-color: #3367d6;
        }
        .tips {
            color: #666;
            font-size: 14px;
            margin-top: 5px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h3 style="padding-bottom: 5px;">合同圖片簽名區(qū)</h3>
        <canvas id="canvas" width="800" height="400"></canvas>
        
        <div class="signature-area">
            <h4>電子簽名</h4>
            <p class="tips">在下方畫布簽名,完成后點(diǎn)擊保存簽名</p>
            <canvas id="signaturePad" width="800" height="150"></canvas>
            
            <!-- 新增簽名操作區(qū) -->
            <div class="signature-actions">
                <button id="clearCurrentSignature">清空當(dāng)前簽名</button>
                <button id="saveSignature">保存簽名到畫布</button>
                <button id="uploadImage">上傳合同圖片</button>
                <input type="file" id="fileInput" accept="image/*" style="display:none">
                <button id="removeSelected">刪除選中簽名</button>
                <button id="clearCanvas">清空畫布</button>
            </div>
        </div>    
    </div>

    <script>
        // 初始化主畫布
        const canvas = new fabric.Canvas('canvas', {
            preserveObjectStacking: true,
            backgroundColor: '#fff',
            enableTouchEvents: true
        });

        // 初始化簽名板
        const signaturePad = document.getElementById('signaturePad');
        const sigCtx = signaturePad.getContext('2d');
        let isDrawing = false;

        // 設(shè)置簽名線條樣式
        sigCtx.lineWidth = 2;
        sigCtx.lineCap = 'round';
        sigCtx.strokeStyle = '#000';

        // 簽名板事件綁定
        signaturePad.addEventListener('mousedown', startDrawing);
        signaturePad.addEventListener('mousemove', draw);
        signaturePad.addEventListener('mouseup', stopDrawing);
        signaturePad.addEventListener('mouseout', stopDrawing);

        // 移動(dòng)端觸摸事件
        signaturePad.addEventListener('touchstart', (e) => {
            const touch = e.touches[0];
            const mouseEvent = new MouseEvent('mousedown', {
                clientX: touch.clientX,
                clientY: touch.clientY
            });
            signaturePad.dispatchEvent(mouseEvent);
        });

        signaturePad.addEventListener('touchmove', (e) => {
            e.preventDefault();
            const touch = e.touches[0];
            const mouseEvent = new MouseEvent('mousemove', {
                clientX: touch.clientX,
                clientY: touch.clientY
            });
            signaturePad.dispatchEvent(mouseEvent);
        });

        signaturePad.addEventListener('touchend', () => {
            const mouseEvent = new MouseEvent('mouseup');
            signaturePad.dispatchEvent(mouseEvent);
        });

        // 簽名繪制函數(shù)
        function startDrawing(e) {
            isDrawing = true;
            const rect = signaturePad.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            sigCtx.beginPath();
            sigCtx.moveTo(x, y);
        }

        function draw(e) {
            if (!isDrawing) return;
            const rect = signaturePad.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            sigCtx.lineTo(x, y);
            sigCtx.stroke();
        }

        function stopDrawing() {
            isDrawing = false;
        }

        // 核心功能:清空當(dāng)前簽名
        document.getElementById('clearCurrentSignature').addEventListener('click', () => {
            // 先判斷是否有簽名內(nèi)容
            const imageData = sigCtx.getImageData(0, 0, signaturePad.width, signaturePad.height);
            const data = imageData.data;
            let hasContent = false;
            
            // 檢查畫布是否有內(nèi)容(跳過全透明像素)
            for (let i = 0; i < data.length; i += 4) {
                if (data[i + 3] > 0) { // alpha通道大于0
                    hasContent = true;
                    break;
                }
            }
            
            if (hasContent) {
                sigCtx.clearRect(0, 0, signaturePad.width, signaturePad.height);
            } else {
                // 可以省略提示,也可以保留
                // alert('簽名區(qū)為空,無需清空');
            }
        });

        // 上傳圖片功能
        document.getElementById('uploadImage').addEventListener('click', () => {
            document.getElementById('fileInput').click();
        });

        document.getElementById('fileInput').addEventListener('change', (e) => {
            const file = e.target.files[0];
            if (!file) return;
            
            const reader = new FileReader();
            reader.onload = (event) => {
                fabric.Image.fromURL(event.target.result, (img) => {
                    const scale = Math.min(
                        canvas.width / img.width,
                        canvas.height / img.height,
                        1
                    );
                    img.scale(scale);
                    img.set({
                        left: (canvas.width - img.width * scale) / 2,
                        top: (canvas.height - img.height * scale) / 2,
                        selectable: false
                    });
                    canvas.add(img);
                    canvas.sendToBack(img);
                });
            };
            reader.readAsDataURL(file);
        });

        // 保存簽名到主畫布
        document.getElementById('saveSignature').addEventListener('click', () => {
            // 檢查簽名是否為空
            const imageData = sigCtx.getImageData(0, 0, signaturePad.width, signaturePad.height);
            const data = imageData.data;
            let hasContent = false;
            
            for (let i = 0; i < data.length; i += 4) {
                if (data[i + 3] > 0) {
                    hasContent = true;
                    break;
                }
            }
            
            if (!hasContent) {
                alert('請(qǐng)先完成簽名再保存');
                return;
            }
            
            const signatureImg = new Image();
            signatureImg.src = signaturePad.toDataURL('image/png');
            
            signatureImg.onload = function() {
                const fabricImg = new fabric.Image(signatureImg, {
                    left: 100,
                    top: 100,
                    opacity: 1,
                    // 啟用縮放功能
                    lockScalingX: false,
                    lockScalingY: false,
                    lockUniScaling: false,
                    // 可選設(shè)置
                    minScaleLimit: 0.3, // 最小縮放比例
                    maxScaleLimit: 3,   // 最大縮放比例
                    isSignature: true
                });
                canvas.add(fabricImg);
                canvas.setActiveObject(fabricImg);
                sigCtx.clearRect(0, 0, signaturePad.width, signaturePad.height);
            };
        });

        // 刪除畫布中選中的簽名
        document.getElementById('removeSelected').addEventListener('click', () => {
            const activeObj = canvas.getActiveObject();
            if (activeObj) {
                canvas.remove(activeObj);
            } else {
                alert('請(qǐng)先點(diǎn)擊選中要?jiǎng)h除的簽名');
            }
        });

        // 清空畫布
        document.getElementById('clearCanvas').addEventListener('click', () => {
            if (canvas.getObjects().length > 0 && confirm('確定要清空所有內(nèi)容嗎?')) {
                canvas.clear();
            }
        });

        // 初始化時(shí)加載示例圖片
        fabric.Image.fromURL('https://picsum.photos/800/600', (img) => {
            img.set({
                left: 0,
                top: 0,
                selectable: false
            });
            canvas.add(img);
            canvas.sendToBack(img);
        });
    </script>
</body>
</html>

踩過的坑和優(yōu)化建議

測(cè)試時(shí)發(fā)現(xiàn),簽名圖片添加到畫布后有時(shí)會(huì)蓋住背景圖片。解決辦法是在添加背景圖片后調(diào)用 canvas.sendToBack (),把它放到最底層。另外,移動(dòng)端適配是個(gè)大問題。需要給 Canvas 添加 touch 事件監(jiān)聽,好在 fabric.js 有專門的 touch 事件處理,稍微配置一下就能用。如果用戶簽名失誤,最好加個(gè) “清除簽名” 按鈕,調(diào)用 canvas.remove (activeObject) 就能刪除當(dāng)前選中的簽名。

最后想說,作為程序員,我們每天都在和各種奇葩需求打交道。與其抱怨,不如像這次用 fabric.js 實(shí)現(xiàn)簽名合成功能一樣,把每次需求都當(dāng)成提升自己的機(jī)會(huì)。畢竟,解決問題的能力才是我們安身立命的根本。

到此這篇關(guān)于用fabric.js搞定電子簽名拖拽合成圖片的文章就介紹到這了,更多相關(guān)fabric.js電子簽名拖拽合成圖片內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • JavaScript如何通過userAgent判斷幾個(gè)常用瀏覽器詳解

    JavaScript如何通過userAgent判斷幾個(gè)常用瀏覽器詳解

    userAgent 屬性是一個(gè)只讀的字符串,聲明了瀏覽器用于 HTTP 請(qǐng)求的用戶代理頭的值,這篇文章主要給大家介紹了關(guān)于JavaScript如何通過userAgent判斷幾個(gè)常用瀏覽器的相關(guān)資料,需要的朋友可以參考下
    2021-06-06
  • JS簡(jiǎn)單獲取日期相差天數(shù)的方法

    JS簡(jiǎn)單獲取日期相差天數(shù)的方法

    這篇文章主要介紹了JS簡(jiǎn)單獲取日期相差天數(shù)的方法,涉及javascript日期時(shí)間的轉(zhuǎn)換與運(yùn)算相關(guān)操作技巧,需要的朋友可以參考下
    2017-04-04
  • 教你5分鐘學(xué)會(huì)用requirejs(必看篇)

    教你5分鐘學(xué)會(huì)用requirejs(必看篇)

    下面小編就為大家?guī)硪黄棠?分鐘學(xué)會(huì)用requirejs(必看篇)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-07-07
  • Javascript匿名函數(shù)的一種應(yīng)用 代碼封裝

    Javascript匿名函數(shù)的一種應(yīng)用 代碼封裝

    說實(shí)話,對(duì)于js初學(xué)者的我來說。這個(gè)東西太嚇人了,在這些JS庫(kù)中,這個(gè)函數(shù)基本上把整個(gè)庫(kù)的所有代碼全括起來了,這種寫法完全超越了我的常識(shí)。
    2010-06-06
  • 基于JavaScript實(shí)現(xiàn)跳轉(zhuǎn)提示頁(yè)面

    基于JavaScript實(shí)現(xiàn)跳轉(zhuǎn)提示頁(yè)面

    這篇文章主要介紹了基于JavaScript實(shí)現(xiàn)跳轉(zhuǎn)提示頁(yè)面 的相關(guān)資料,需要的朋友可以參考下
    2016-09-09
  • JS小功能(onmouseover實(shí)現(xiàn)選擇月份)實(shí)例代碼

    JS小功能(onmouseover實(shí)現(xiàn)選擇月份)實(shí)例代碼

    這篇文章主要介紹了onmouseover實(shí)現(xiàn)選擇月份實(shí)例代碼,有需要的朋友可以參考一下
    2013-11-11
  • echarts學(xué)習(xí)筆記之箱線圖的分析與繪制詳解

    echarts學(xué)習(xí)筆記之箱線圖的分析與繪制詳解

    最近在學(xué)習(xí)echarts,所以下面這篇文章主要給大家介紹了關(guān)于echarts學(xué)習(xí)筆記之箱線圖的分析與繪制的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-11-11
  • ES6新特性二:Iterator(遍歷器)和for-of循環(huán)詳解

    ES6新特性二:Iterator(遍歷器)和for-of循環(huán)詳解

    這篇文章主要介紹了ES6新特性二:Iterator(遍歷器)和for-of循環(huán),結(jié)合實(shí)例形式分析了ES6中Iterator(遍歷器)和for-of循環(huán)遍歷操作的相關(guān)實(shí)現(xiàn)技巧與注意事項(xiàng),需要的朋友可以參考下
    2017-04-04
  • JavaScript查看數(shù)據(jù)返回值的方法

    JavaScript查看數(shù)據(jù)返回值的方法

    console.log()、alert()?都是JavaScript中用于在開發(fā)過程中輸出信息或向用戶顯示消息的不同方法,它們的主要區(qū)別在于目的、使用場(chǎng)景以及展示方式,本文將給大家介紹JavaScript?中怎么看數(shù)據(jù)返回值,需要的朋友可以參考下
    2024-07-07
  • 淺談webpack組織模塊的原理

    淺談webpack組織模塊的原理

    這篇文章主要介紹了淺談webpack組織模塊的原理,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-03-03

最新評(píng)論

万宁市| 灵寿县| 延边| 罗城| 洞头县| 华池县| 铁岭县| 安阳市| 浪卡子县| 临江市| 繁昌县| 邵东县| 岑溪市| 剑河县| 兴国县| 卓资县| 石城县| 塔城市| 长顺县| 临泉县| 江达县| 福建省| 西贡区| 诸城市| 伊吾县| 郯城县| 旅游| 当阳市| 鄂托克前旗| 乐昌市| 双辽市| 楚雄市| 桃江县| 石林| 石台县| 泸西县| 广丰县| 韶关市| 加查县| 阿合奇县| 望奎县|