前端Canvas实现电子签名功能

一、前言

在日常网站开发中,很多场景都需要用到电子签名功能,比如在线表单、合同签署、报名系统、后台确认签字等。

传统的图片上传签名方式繁琐又不美观,而使用前端 Canvas 实现手写签名,无需后端接口、纯前端实现、轻量化无依赖,同时完美兼容电脑鼠标和手机触屏,体验堪比专业签名工具。

分享一套我封装好的完整版 Canvas 电子签名组件,代码简洁、注释详细、开箱即用。

二、功能亮点

本次实现的签名功能,集成了日常开发所需的全部核心能力,适配多端、体验顺滑:

  • 双端兼容:支持电脑鼠标绘制、手机触屏手写,无适配bug
  • 顺滑笔迹:圆角画笔处理,模拟真实签字笔书写效果,无锯齿、无卡顿
  • 自定义笔迹:支持黑、蓝、红三种常用签名颜色一键切换
  • 基础核心操作:一键清空画布、一键导出PNG透明底签名图片
  • 防干扰优化:禁止画布区域滚动,解决移动端手写偏移、滑动冲突问题
  • 零依赖:原生HTML+CSS+JS开发,不依赖任何框架,直接打开即可运行

三、完整源码(开箱即用)

以下是完整可运行代码,直接复制保存为 .html 文件,或嵌入博客、项目页面中即可使用。代码经过优化,兼容所有现代浏览器。

html 复制代码
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <!-- 移动端完美适配 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Canvas 电子签名工具</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: "Microsoft Yahei", system-ui, sans-serif;
        }

        body {
            padding: 30px 20px;
            max-width: 800px;
            margin: 0 auto;
            background-color: #f5f7fa;
        }

        h3 {
            color: #333;
            margin-bottom: 16px;
            font-size: 20px;
        }

        /* 签名画布容器 */
        .sign-box {
            border: 2px solid #e5e7eb;
            border-radius: 10px;
            overflow: hidden;
            background: #fff;
            touch-action: none; /* 禁止移动端滚动干扰手写 */
            box-shadow: 0 2px 12px rgba(0,0,0,0.05);
        }

        #signCanvas {
            display: block;
            background: #ffffff;
            cursor: crosshair;
        }

        /* 按钮操作区域 */
        .btn-group {
            margin-top: 20px;
            display: flex;
            gap: 12px;
            flex-wrap: wrap;
            align-items: center;
        }

        button {
            padding: 10px 24px;
            border: none;
            border-radius: 6px;
            font-size: 15px;
            cursor: pointer;
            transition: all 0.2s ease;
        }

        button:hover {
            opacity: 0.9;
            transform: translateY(-2px);
        }

        .clear-btn {
            background-color: #f53f3f;
            color: #fff;
        }

        .save-btn {
            background-color: #2b7de9;
            color: #fff;
        }

        /* 颜色选择区域 */
        .color-wrap {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-left: 10px;
        }

        .color-text {
            color: #666;
            font-size: 14px;
        }

        .color-item {
            width: 30px;
            height: 30px;
            border-radius: 50%;
            cursor: pointer;
            border: 2px solid transparent;
            transition: 0.2s;
        }

        .color-item.active {
            border-color: #222;
            transform: scale(1.1);
        }
    </style>
</head>
<body>
    <h3>在线电子签名(鼠标/触屏均可手写)</h3>
    <div class="sign-box">
        <canvas id="signCanvas" width="750" height="400"></canvas>
    </div>

    <div class="btn-group">
        <button class="clear-btn" id="clearBtn">清空签名</button>
        <button class="save-btn" id="saveBtn">保存签名图片</button>
        <div class="color-wrap">
            <span class="color-text">笔迹颜色:</span>
            <div class="color-item active" style="background:#000" data-color="#000"></div>
            <div class="color-item" style="background:#0984e3" data-color="#0984e3"></div>
            <div class="color-item" style="background:#d63031" data-color="#d63031"></div>
        </div>
    </div>

    <script>
        // 获取画布元素和上下文
        const canvas = document.getElementById('signCanvas');
        const ctx = canvas.getContext('2d');

        // 全局状态变量
        let isDrawing = false; // 是否正在绘制
        let lastX = 0; // 上一次绘制X坐标
        let lastY = 0; // 上一次绘制Y坐标
        let strokeColor = "#000"; // 默认笔迹颜色
        const lineWidth = 3; // 固定笔迹粗细

        // 初始化画笔样式(顺滑圆角笔迹)
        function initCanvasStyle() {
            ctx.lineCap = "round"; // 线条两端圆角
            ctx.lineJoin = "round"; // 线条连接处圆角
            ctx.lineWidth = lineWidth;
            ctx.strokeStyle = strokeColor;
        }
        initCanvasStyle();

        // 统一获取鼠标/触屏坐标(修正画布偏移,解决错位问题)
        function getCanvasPos(e) {
            const rect = canvas.getBoundingClientRect();
            // 兼容移动端触屏事件
            if (e.touches) {
                return {
                    x: e.touches[0].clientX - rect.left,
                    y: e.touches[0].clientY - rect.top
                }
            }
            // 兼容电脑鼠标事件
            return {
                x: e.clientX - rect.left,
                y: e.clientY - rect.top
            }
        }

        // 开始绘制
        function startDraw(e) {
            e.preventDefault();
            isDrawing = true;
            const pos = getCanvasPos(e);
            lastX = pos.x;
            lastY = pos.y;
        }

        // 绘制过程
        function drawing(e) {
            if (!isDrawing) return;
            e.preventDefault();
            const pos = getCanvasPos(e);

            ctx.beginPath();
            ctx.moveTo(lastX, lastY);
            ctx.lineTo(pos.x, pos.y);
            ctx.stroke();

            // 更新坐标,实现连续绘制
            lastX = pos.x;
            lastY = pos.y;
        }

        // 停止绘制
        function stopDraw() {
            isDrawing = false;
        }

        // 绑定鼠标事件(电脑端)
        canvas.addEventListener('mousedown', startDraw);
        canvas.addEventListener('mousemove', drawing);
        canvas.addEventListener('mouseup', stopDraw);
        canvas.addEventListener('mouseout', stopDraw);

        // 绑定触屏事件(手机/平板端)
        canvas.addEventListener('touchstart', startDraw);
        canvas.addEventListener('touchmove', drawing);
        canvas.addEventListener('touchend', stopDraw);

        // 清空画布功能
        document.getElementById('clearBtn').addEventListener('click', () => {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
        });

        // 保存签名为PNG图片
        document.getElementById('saveBtn').addEventListener('click', () => {
            const imgUrl = canvas.toDataURL('image/png');
            const aTag = document.createElement('a');
            aTag.href = imgUrl;
            aTag.download = '个人签名.png';
            aTag.click();
        });

        // 切换笔迹颜色
        document.querySelectorAll('.color-item').forEach(item => {
            item.addEventListener('click', () => {
                // 移除所有选中状态
                document.querySelectorAll('.color-item').forEach(el => el.classList.remove('active'));
                // 添加当前选中状态
                item.classList.add('active');
                // 更新画笔颜色
                strokeColor = item.dataset.color;
                ctx.strokeStyle = strokeColor;
            })
        })
    </script>
</body>
</html>

四、代码核心原理讲解

  1. 画布样式优化
    通过 lineCap: round 和 lineJoin: round 将画笔设置为圆角样式,解决原生 Canvas 绘制线条锯齿、生硬的问题,完美模拟手写真实效果。同时设置 touch-action: none 杜绝移动端滑动页面和手写冲突的bug。
  2. 双端坐标兼容
    单独封装了坐标获取函数,同时适配鼠标事件和触屏事件,并且通过 getBoundingClientRect() 获取画布真实偏移位置,彻底解决画布绘制错位、坐标偏移的常见问题。
  3. 核心绘制逻辑
    通过 mousedown/touchstart 触发绘制开始,记录初始坐标;mousemove/touchmove 实时连线绘制;mouseup/touchend 终止绘制,实现连续手写效果。
  4. 图片导出原理
    利用 Canvas 原生 API toDataURL() 将画布内容转为图片 Base64 链接,通过动态创建 a 标签实现一键下载,无需后端参与,纯前端完成导出。