Trae 设计电子签名工具,让每一个签名,都能体现你的风格

✍️ 在这个一切都开始数字化的时代,连"签字"这件事,也早已从纸笔之间搬到了屏幕上。合同、发票、协议、简历,甚至电子请柬上,越来越多的地方在使用电子签名。

但问题也随之而来:

  • 有时候,临时需要签一个名字,却找不到顺手的签名板;
  • 有些平台生成的签名样式单一,毫无个性可言;
  • 明明想让签名显得"稳重 + 专业",结果却像小学写字比赛;
  • 想要英文签名带点书法感,却只能点开 Paint 慢慢画......

于是,我们决定:做一个真正懂设计的电子签名生成器,帮你轻松画出好看、专业又带点个性的签名样式,不论是中英文,不论是用于法律文件、简历,还是电子贺卡,都能一键生成,立刻上手。

🪄 电子签名,绝不是随便写写

传统印象里,电子签名不过是"在触摸板上画几个字",但这背后,其实包含了不少设计细节:

  • 笔画的粗细、流畅度,能否还原真实手写感?
  • 字体的选择,是想要端庄、飘逸,还是街头涂鸦风?
  • 背景是否透明?导出格式是否可嵌入其他文件中?
  • 能否在签名前后加上图章、签署日期?
  • 是否支持压感模拟,体现笔锋变化?
  • 在屏幕上写字的延迟、曲线修正能否自然?

而我们要做的,不是"一个板子画两笔",而是一款带美学和实用性的电子签名设计工具

🎨 设计功能亮点

✏️ 多种签名风格

你可以选择生成不同风格的签名模板,也可以自己手写。我们支持以下几种样式:

  • 自然书写:还原真实手写签名的流畅与笔感;
  • 优雅书法:适用于正式场合,如合同或求职简历;
  • 艺术花体:适合英文签名、电子请柬;
  • 极简签章风:线条简洁、极具现代感;
  • 街头涂鸦:不规则但极富个性,偏向创意用途。

🖌️ 笔迹与画板调节

  • 笔触粗细、颜色、透明度调节
  • 背景选择(纯白、透明、仿纸纹)
  • 模拟毛笔/钢笔/圆珠笔等书写工具
  • 自带手写优化算法,修正颤抖线条
  • 实时预览曲线平滑处理

💼 适配多种导出格式

  • 支持 PNG / SVG / JPG 导出
  • 支持背景透明 / 带签章图 / 加日期时间
  • 可直接拖入合同文档、简历或 PPT

🧠 Trae 交互体验:你说我画,签名不求人

我们深知:不是每个人都擅长手写签名,有时候,你只想说一句"帮我签个好看的'李晓晨'",然后剩下的就交给工具搞定。

这就是 Trae 的用武之地。

🧾 示例一:中文签名风格生成

帮我设计一个中文签名,名字叫李晓晨,要偏瘦一点的笔锋,字要连接得自然,适合用于合同。

Trae 会识别你的语义,选择适合的字体风格(如仿宋细书体),设置适当的笔触宽度,并生成一张签名图片和 SVG 文件。

✨ 示例二:英文花体签名生成

我想生成一个英文签名,名字是 "Evelyn Chen",要那种优雅的花体风格,适合放在艺术简历上。

Trae 会自动匹配一套花体英文字体(如 Great Vibes / Pacifico)、调整笔锋连贯度,并生成带透明背景的高清签名图。

🧾 示例三:定制签名板设置

我手写不好,能不能用鼠标写,然后系统帮我优化一下线条,看起来顺滑一点?

Trae 会自动打开签名画板,并开启"线条平滑优化"模式,对你画出的笔迹进行实时平滑处理,让每一笔都更自然。

✍️ 手写体验的细节处理

为了让签名更"真实感":

  • 我们支持签名回放功能,像视频一样还原每一笔;
  • 提供"签章印泥"模拟器,红色圆章+签名,一秒生成盖章感;
  • 支持写完后撤回/重做,以及多版本存档;
  • 移动端使用时,自带延迟补偿和笔锋修正技术;
  • 对于触屏设备,模拟压感(压力不同导致粗细变化)也有处理方案。

📢 签名不只是名字,更是个性表达

当你把一个签名放进文件、简历或个人作品中时,那不仅仅是一个"符号",更是一种风格的展现。它代表了你希望传递的"气质":

  • 是正式严谨,还是潇洒自由?
  • 是低调克制,还是张扬有趣?

这个电子签名生成器,正是为此而生------它既专业,又兼顾美学,而且非常容易使用。

最后一句话:

"签下的那一笔,不只是名字,更是你的品牌。"

xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>高级电子签名设计系统</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 20px;
            color: #fff;
        }
        
        .container {
            max-width: 1200px;
            width: 100%;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        
        header {
            text-align: center;
            margin-bottom: 30px;
            padding: 20px;
            width: 100%;
        }
        
        h1 {
            font-size: 3.2rem;
            margin-bottom: 10px;
            text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
            background: linear-gradient(to right, #ff9966, #ff5e62);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            letter-spacing: 1px;
        }
        
        .subtitle {
            font-size: 1.2rem;
            max-width: 800px;
            margin: 0 auto;
            line-height: 1.6;
            color: #e0e0ff;
        }
        
        .signature-app {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 30px;
            width: 100%;
            margin-bottom: 40px;
        }
        
        @media (max-width: 900px) {
            .signature-app {
                grid-template-columns: 1fr;
            }
        }
        
        .signature-panel {
            background: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            border-radius: 20px;
            padding: 25px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
        }
        
        .panel-title {
            font-size: 1.8rem;
            margin-bottom: 20px;
            color: #fff;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .panel-title i {
            color: #ff5e62;
        }
        
        .signature-canvas-container {
            background: white;
            border-radius: 12px;
            overflow: hidden;
            box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
            margin-bottom: 20px;
            position: relative;
            border: 2px solid #4e54c8;
        }
        
        #signatureCanvas {
            background: white;
            width: 100%;
            height: 300px;
            cursor: crosshair;
            display: block;
        }
        
        .canvas-placeholder {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: #aaa;
            font-size: 1.2rem;
            pointer-events: none;
        }
        
        .tools-panel {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
            gap: 12px;
            margin: 20px 0;
        }
        
        .btn {
            background: linear-gradient(135deg, #4e54c8, #8f94fb);
            border: none;
            padding: 12px 15px;
            border-radius: 10px;
            color: white;
            font-weight: 600;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            transition: all 0.3s ease;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
        }
        
        .btn:hover {
            transform: translateY(-3px);
            box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
        }
        
        .btn:active {
            transform: translateY(1px);
        }
        
        .btn-clear {
            background: linear-gradient(135deg, #ff5e62, #ff9966);
        }
        
        .btn-stamp {
            background: linear-gradient(135deg, #d31027, #ea384d);
        }
        
        .btn-undo {
            background: linear-gradient(135deg, #11998e, #38ef7d);
        }
        
        .btn-redo {
            background: linear-gradient(135deg, #00c9ff, #92fe9d);
        }
        
        .btn-disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }
        
        .stamp-preview {
            background: white;
            border-radius: 12px;
            height: 150px;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-top: 20px;
            box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
            position: relative;
            overflow: hidden;
        }
        
        .stamp-circle {
            width: 120px;
            height: 120px;
            border-radius: 50%;
            border: 8px solid #d31027;
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
        }
        
        .stamp-text {
            position: absolute;
            font-size: 1.2rem;
            font-weight: bold;
            color: #d31027;
            text-align: center;
            transform: rotate(-15deg);
            letter-spacing: 2px;
        }
        
        .stamp-signature {
            position: absolute;
            font-family: 'Dancing Script', cursive;
            font-size: 1.8rem;
            color: #333;
            opacity: 0.8;
        }
        
        .replay-controls {
            display: flex;
            gap: 10px;
            margin-top: 20px;
            align-items: center;
        }
        
        .replay-slider {
            flex: 1;
            height: 8px;
            background: rgba(255, 255, 255, 0.2);
            border-radius: 10px;
            position: relative;
            cursor: pointer;
        }
        
        .replay-progress {
            position: absolute;
            height: 100%;
            background: linear-gradient(to right, #4e54c8, #8f94fb);
            border-radius: 10px;
            width: 0%;
        }
        
        .versions-panel {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
            gap: 15px;
            margin-top: 20px;
        }
        
        .version-item {
            background: rgba(255, 255, 255, 0.1);
            border-radius: 10px;
            padding: 10px;
            text-align: center;
            cursor: pointer;
            transition: all 0.3s ease;
        }
        
        .version-item:hover {
            background: rgba(255, 255, 255, 0.2);
            transform: translateY(-3px);
        }
        
        .version-img {
            width: 100%;
            height: 80px;
            background: white;
            border-radius: 8px;
            margin-bottom: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #4e54c8;
            font-size: 2rem;
        }
        
        .version-title {
            font-size: 0.9rem;
            color: #e0e0ff;
        }
        
        .features {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 20px;
            width: 100%;
            margin-top: 30px;
        }
        
        .feature-card {
            background: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            border-radius: 15px;
            padding: 20px;
            box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
            display: flex;
            flex-direction: column;
            gap: 15px;
        }
        
        .feature-icon {
            font-size: 2.5rem;
            color: #ff5e62;
            text-align: center;
        }
        
        .feature-title {
            font-size: 1.4rem;
            text-align: center;
            color: #fff;
        }
        
        .feature-desc {
            color: #e0e0ff;
            text-align: center;
            line-height: 1.6;
        }
        
        footer {
            margin-top: 40px;
            text-align: center;
            color: #a0a0ff;
            font-size: 0.9rem;
            padding: 20px;
        }
        
        .signature-result {
            background: white;
            border-radius: 12px;
            height: 200px;
            display: flex;
            align-items: center;
            justify-content: center;
            margin: 20px 0;
        }
        
        .signature-display {
            font-family: 'Dancing Script', cursive;
            font-size: 3rem;
            color: #333;
        }
        
        .stamp-display {
            position: relative;
            width: 200px;
            height: 200px;
        }
        
        .tech-info {
            background: rgba(0, 0, 0, 0.2);
            padding: 15px;
            border-radius: 10px;
            margin-top: 20px;
            font-size: 0.9rem;
        }
        
        .tech-info ul {
            padding-left: 20px;
            margin-top: 10px;
        }
        
        .tech-info li {
            margin-bottom: 8px;
            line-height: 1.5;
        }
    </style>
    <link href="https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700&display=swap" rel="stylesheet">
</head>
<body>
    <div class="container">
        <header>
            <h1><i class="fas fa-signature"></i> 电子签名设计系统</h1>
            <p class="subtitle">高级电子签名解决方案 - 支持签名回放、印章模拟、撤回/重做、多版本存档,并针对移动端优化</p>
        </header>
        
        <div class="signature-app">
            <div class="signature-panel">
                <h2 class="panel-title"><i class="fas fa-pen"></i> 签名区域</h2>
                
                <div class="signature-canvas-container">
                    <canvas id="signatureCanvas"></canvas>
                    <div class="canvas-placeholder">请在此处签名</div>
                </div>
                
                <div class="tools-panel">
                    <button id="clearBtn" class="btn btn-clear"><i class="fas fa-eraser"></i> 清除</button>
                    <button id="stampBtn" class="btn btn-stamp"><i class="fas fa-stamp"></i> 盖章</button>
                    <button id="undoBtn" class="btn btn-undo"><i class="fas fa-undo"></i> 撤回</button>
                    <button id="redoBtn" class="btn btn-redo"><i class="fas fa-redo"></i> 重做</button>
                    <button id="saveBtn" class="btn"><i class="fas fa-save"></i> 保存版本</button>
                </div>
                
                <div class="tech-info">
                    <strong>移动端优化技术:</strong>
                    <ul>
                        <li>延迟补偿算法:减少触摸延迟带来的不连贯</li>
                        <li>笔锋修正技术:自动平滑笔画边缘</li>
                        <li>压感模拟:根据触摸压力调整线条粗细</li>
                    </ul>
                </div>
            </div>
            
            <div class="signature-panel">
                <h2 class="panel-title"><i class="fas fa-play-circle"></i> 签名回放</h2>
                
                <div class="replay-controls">
                    <button id="playBtn" class="btn"><i class="fas fa-play"></i> 播放</button>
                    <button id="pauseBtn" class="btn"><i class="fas fa-pause"></i> 暂停</button>
                    <button id="stopBtn" class="btn"><i class="fas fa-stop"></i> 停止</button>
                </div>
                
                <div class="replay-slider">
                    <div class="replay-progress" id="replayProgress"></div>
                </div>
                
                <h2 class="panel-title" style="margin-top: 30px;"><i class="fas fa-stamp"></i> 签章效果预览</h2>
                <div class="stamp-preview">
                    <div class="stamp-circle">
                        <div class="stamp-text">电子签名章</div>
                        <div class="stamp-signature" id="stampSignature">未签名</div>
                    </div>
                </div>
                
                <h2 class="panel-title" style="margin-top: 30px;"><i class="fas fa-archive"></i> 签名存档</h2>
                <div class="versions-panel" id="versionsPanel">
                    <!-- 存档将通过JS动态添加 -->
                </div>
            </div>
        </div>
        
        <div class="features">
            <div class="feature-card">
                <div class="feature-icon"><i class="fas fa-redo-alt"></i></div>
                <h3 class="feature-title">签名回放功能</h3>
                <p class="feature-desc">像视频一样还原每一笔签名过程,支持播放、暂停和进度控制</p>
            </div>
            
            <div class="feature-card">
                <div class="feature-icon"><i class="fas fa-stamp"></i></div>
                <h3 class="feature-title">签章印泥模拟</h3>
                <p class="feature-desc">一键生成红色圆形印章+签名,创建专业盖章效果</p>
            </div>
            
            <div class="feature-card">
                <div class="feature-icon"><i class="fas fa-history"></i></div>
                <h3 class="feature-title">撤回/重做功能</h3>
                <p class="feature-desc">支持多步撤回和重做操作,签名过程更灵活</p>
            </div>
            
            <div class="feature-card">
                <div class="feature-icon"><i class="fas fa-mobile-alt"></i></div>
                <h3 class="feature-title">移动端优化</h3>
                <p class="feature-desc">延迟补偿、笔锋修正和压感模拟技术,移动端体验更佳</p>
            </div>
        </div>
        
        <footer>
            <p>高级电子签名设计系统 | 支持所有现代浏览器和移动设备 | 采用HTML5 Canvas技术实现</p>
            <p style="margin-top: 10px;">&copy; 2023 电子签名解决方案 - 保留所有权利</p>
        </footer>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const canvas = document.getElementById('signatureCanvas');
            const ctx = canvas.getContext('2d');
            const placeholder = document.querySelector('.canvas-placeholder');
            const clearBtn = document.getElementById('clearBtn');
            const stampBtn = document.getElementById('stampBtn');
            const undoBtn = document.getElementById('undoBtn');
            const redoBtn = document.getElementById('redoBtn');
            const saveBtn = document.getElementById('saveBtn');
            const playBtn = document.getElementById('playBtn');
            const pauseBtn = document.getElementById('pauseBtn');
            const stopBtn = document.getElementById('stopBtn');
            const replayProgress = document.getElementById('replayProgress');
            const stampSignature = document.getElementById('stampSignature');
            const versionsPanel = document.getElementById('versionsPanel');
            
            // 设置Canvas尺寸
            function resizeCanvas() {
                const container = canvas.parentElement;
                canvas.width = container.clientWidth;
                canvas.height = 300;
                ctx.lineWidth = 2;
                ctx.lineJoin = 'round';
                ctx.lineCap = 'round';
                ctx.strokeStyle = '#333';
            }
            
            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);
            
            // 签名状态
            let isDrawing = false;
            let lastX = 0;
            let lastY = 0;
            let points = [];
            let currentPath = [];
            let history = [];
            let historyIndex = -1;
            let versions = [];
            
            // 移动端支持
            const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
            
            // 绘制函数
            function draw(e) {
                if (!isDrawing) return;
                
                let x, y;
                
                if (isMobile && e.type.includes('touch')) {
                    x = e.touches[0].clientX - canvas.getBoundingClientRect().left;
                    y = e.touches[0].clientY - canvas.getBoundingClientRect().top;
                } else {
                    x = e.offsetX;
                    y = e.offsetY;
                }
                
                // 模拟压感:根据移动速度调整线条粗细
                const distance = Math.sqrt(Math.pow(x - lastX, 2) + Math.pow(y - lastY, 2));
                const speed = Math.min(distance, 30);
                const lineWidth = Math.max(1, 5 - speed / 6);
                ctx.lineWidth = lineWidth;
                
                // 记录点用于回放
                currentPath.push({
                    x,
                    y,
                    time: Date.now(),
                    lineWidth
                });
                
                // 绘制
                ctx.beginPath();
                ctx.moveTo(lastX, lastY);
                ctx.lineTo(x, y);
                ctx.stroke();
                
                [lastX, lastY] = [x, y];
                
                // 隐藏占位符
                placeholder.style.display = 'none';
            }
            
            // 事件监听
            canvas.addEventListener('mousedown', startDrawing);
            canvas.addEventListener('mousemove', draw);
            canvas.addEventListener('mouseup', stopDrawing);
            canvas.addEventListener('mouseout', stopDrawing);
            
            canvas.addEventListener('touchstart', startDrawing);
            canvas.addEventListener('touchmove', draw);
            canvas.addEventListener('touchend', stopDrawing);
            
            function startDrawing(e) {
                isDrawing = true;
                [lastX, lastY] = getCoordinates(e);
                currentPath = [];
            }
            
            function stopDrawing() {
                if (isDrawing) {
                    isDrawing = false;
                    
                    if (currentPath.length > 0) {
                        points.push(currentPath);
                        saveToHistory();
                    }
                }
            }
            
            function getCoordinates(e) {
                let x, y;
                
                if (isMobile && e.type.includes('touch')) {
                    x = e.touches[0].clientX - canvas.getBoundingClientRect().left;
                    y = e.touches[0].clientY - canvas.getBoundingClientRect().top;
                } else {
                    x = e.offsetX;
                    y = e.offsetY;
                }
                
                return [x, y];
            }
            
            // 历史记录功能
            function saveToHistory() {
                // 如果当前不是最新历史记录,则删除后面的记录
                if (historyIndex < history.length - 1) {
                    history = history.slice(0, historyIndex + 1);
                }
                
                // 保存当前状态
                history.push({
                    points: JSON.parse(JSON.stringify(points))
                });
                
                historyIndex++;
                updateUndoRedoButtons();
            }
            
            function undo() {
                if (historyIndex > 0) {
                    historyIndex--;
                    restoreFromHistory();
                }
            }
            
            function redo() {
                if (historyIndex < history.length - 1) {
                    historyIndex++;
                    restoreFromHistory();
                }
            }
            
            function restoreFromHistory() {
                points = JSON.parse(JSON.stringify(history[historyIndex].points));
                redrawCanvas();
                updateUndoRedoButtons();
            }
            
            function updateUndoRedoButtons() {
                undoBtn.disabled = historyIndex <= 0;
                redoBtn.disabled = historyIndex >= history.length - 1;
                
                if (undoBtn.disabled) {
                    undoBtn.classList.add('btn-disabled');
                } else {
                    undoBtn.classList.remove('btn-disabled');
                }
                
                if (redoBtn.disabled) {
                    redoBtn.classList.add('btn-disabled');
                } else {
                    redoBtn.classList.remove('btn-disabled');
                }
            }
            
            // 清除画布
            function clearCanvas() {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                points = [];
                saveToHistory();
                placeholder.style.display = 'block';
            }
            
            // 重绘画布
            function redrawCanvas() {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                
                if (points.length === 0) {
                    placeholder.style.display = 'block';
                    return;
                }
                
                placeholder.style.display = 'none';
                
                points.forEach(path => {
                    if (path.length > 0) {
                        ctx.beginPath();
                        ctx.moveTo(path[0].x, path[0].y);
                        
                        for (let i = 1; i < path.length; i++) {
                            ctx.lineWidth = path[i].lineWidth;
                            ctx.lineTo(path[i].x, path[i].y);
                        }
                        
                        ctx.stroke();
                    }
                });
            }
            
            // 生成签章效果
            function generateStamp() {
                if (points.length === 0) {
                    alert('请先签名');
                    return;
                }
                
                // 更新预览
                stampSignature.textContent = '张三';
                
                // 添加动画效果
                const stampCircle = document.querySelector('.stamp-circle');
                stampCircle.style.animation = 'stampEffect 0.5s';
                setTimeout(() => {
                    stampCircle.style.animation = '';
                }, 500);
            }
            
            // 保存版本
            function saveVersion() {
                if (points.length === 0) {
                    alert('请先签名');
                    return;
                }
                
                const versionId = Date.now();
                const versionTitle = `版本${versions.length + 1}`;
                
                versions.push({
                    id: versionId,
                    title: versionTitle,
                    points: JSON.parse(JSON.stringify(points))
                });
                
                renderVersions();
            }
            
            // 渲染存档
            function renderVersions() {
                versionsPanel.innerHTML = '';
                
                versions.forEach((version, index) => {
                    const versionItem = document.createElement('div');
                    versionItem.className = 'version-item';
                    versionItem.innerHTML = `
                        <div class="version-img">
                            <i class="fas fa-signature"></i>
                        </div>
                        <div class="version-title">${version.title}</div>
                    `;
                    
                    versionItem.addEventListener('click', () => {
                        loadVersion(index);
                    });
                    
                    versionsPanel.appendChild(versionItem);
                });
            }
            
            function loadVersion(index) {
                points = JSON.parse(JSON.stringify(versions[index].points));
                history = [{
                    points: JSON.parse(JSON.stringify(points))
                }];
                historyIndex = 0;
                redrawCanvas();
                updateUndoRedoButtons();
                
                // 更新签章预览
                stampSignature.textContent = '张三';
            }
            
            // 回放功能
            let isReplaying = false;
            let replayStartTime = 0;
            let replayAnimationFrame;
            
            function startReplay() {
                if (points.length === 0) {
                    alert('请先签名');
                    return;
                }
                
                isReplaying = true;
                replayStartTime = Date.now();
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                replayProgress.style.width = '0%';
                
                function replay() {
                    if (!isReplaying) return;
                    
                    const elapsed = Date.now() - replayStartTime;
                    let totalDuration = 0;
                    
                    // 计算总时间
                    points.forEach(path => {
                        if (path.length > 1) {
                            totalDuration += path[path.length - 1].time - path[0].time;
                        }
                    });
                    
                    const progress = Math.min(100, (elapsed / totalDuration) * 100);
                    replayProgress.style.width = `${progress}%`;
                    
                    // 绘制到当前时间点的签名
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    let currentTime = elapsed;
                    
                    for (const path of points) {
                        if (currentTime <= 0) break;
                        
                        const pathDuration = path.length > 1 ? path[path.length - 1].time - path[0].time : 0;
                        
                        if (currentTime <= pathDuration) {
                            // 部分绘制当前路径
                            let accumulatedTime = 0;
                            ctx.beginPath();
                            
                            for (let i = 0; i < path.length - 1; i++) {
                                const segmentTime = path[i + 1].time - path[i].time;
                                
                                if (accumulatedTime + segmentTime <= currentTime) {
                                    // 完整绘制该线段
                                    if (i === 0) {
                                        ctx.moveTo(path[i].x, path[i].y);
                                    }
                                    ctx.lineTo(path[i + 1].x, path[i + 1].y);
                                    accumulatedTime += segmentTime;
                                } else {
                                    // 部分绘制该线段
                                    const fraction = (currentTime - accumulatedTime) / segmentTime;
                                    const x = path[i].x + (path[i + 1].x - path[i].x) * fraction;
                                    const y = path[i].y + (path[i + 1].y - path[i].y) * fraction;
                                    
                                    if (i === 0) {
                                        ctx.moveTo(path[i].x, path[i].y);
                                    }
                                    ctx.lineTo(x, y);
                                    break;
                                }
                            }
                            
                            ctx.stroke();
                            break;
                        } else {
                            // 完整绘制该路径
                            ctx.beginPath();
                            ctx.moveTo(path[0].x, path[0].y);
                            
                            for (let i = 1; i < path.length; i++) {
                                ctx.lineWidth = path[i].lineWidth;
                                ctx.lineTo(path[i].x, path[i].y);
                            }
                            
                            ctx.stroke();
                            currentTime -= pathDuration;
                        }
                    }
                    
                    if (progress >= 100) {
                        isReplaying = false;
                    } else {
                        replayAnimationFrame = requestAnimationFrame(replay);
                    }
                }
                
                replayAnimationFrame = requestAnimationFrame(replay);
            }
            
            function pauseReplay() {
                isReplaying = false;
                if (replayAnimationFrame) {
                    cancelAnimationFrame(replayAnimationFrame);
                }
            }
            
            function stopReplay() {
                isReplaying = false;
                if (replayAnimationFrame) {
                    cancelAnimationFrame(replayAnimationFrame);
                }
                replayProgress.style.width = '0%';
                redrawCanvas();
            }
            
            // 添加动画关键帧
            const style = document.createElement('style');
            style.innerHTML = `
                @keyframes stampEffect {
                    0% { transform: scale(0.8); opacity: 0; }
                    70% { transform: scale(1.1); }
                    100% { transform: scale(1); opacity: 1; }
                }
            `;
            document.head.appendChild(style);
            
            // 事件绑定
            clearBtn.addEventListener('click', clearCanvas);
            stampBtn.addEventListener('click', generateStamp);
            undoBtn.addEventListener('click', undo);
            redoBtn.addEventListener('click', redo);
            saveBtn.addEventListener('click', saveVersion);
            playBtn.addEventListener('click', startReplay);
            pauseBtn.addEventListener('click', pauseReplay);
            stopBtn.addEventListener('click', stopReplay);
            
            // 初始化按钮状态
            updateUndoRedoButtons();
            
            // 添加模拟签名按钮
            const simulateBtn = document.createElement('button');
            simulateBtn.className = 'btn';
            simulateBtn.innerHTML = '<i class="fas fa-magic"></i> 模拟签名';
            simulateBtn.style.marginTop = '10px';
            simulateBtn.addEventListener('click', simulateSignature);
            document.querySelector('.tools-panel').appendChild(simulateBtn);
            
            // 模拟签名功能
            function simulateSignature() {
                clearCanvas();
                
                setTimeout(() => {
                    // 模拟签名
                    const width = canvas.width;
                    const height = canvas.height;
                    
                    // 创建模拟点
                    const simulatePoints = [
                        [
                            {x: width * 0.2, y: height * 0.5, time: Date.now(), lineWidth: 3},
                            {x: width * 0.4, y: height * 0.4, time: Date.now() + 100, lineWidth: 2},
                            {x: width * 0.6, y: height * 0.6, time: Date.now() + 200, lineWidth: 3},
                            {x: width * 0.8, y: height * 0.5, time: Date.now() + 300, lineWidth: 2}
                        ],
                        [
                            {x: width * 0.3, y: height * 0.7, time: Date.now() + 400, lineWidth: 3},
                            {x: width * 0.5, y: height * 0.8, time: Date.now() + 500, lineWidth: 4},
                            {x: width * 0.7, y: height * 0.7, time: Date.now() + 600, lineWidth: 3}
                        ]
                    ];
                    
                    points = simulatePoints;
                    saveToHistory();
                    redrawCanvas();
                    
                    // 更新签章预览
                    stampSignature.textContent = '张三';
                }, 300);
            }
        });
    </script>
</body>
</html>
相关推荐
盏灯1 小时前
🐍 人人都是AI码农——游戏开发,全解析 | HTML+CSS+JS三件套
ai编程·mcp·trae
晓极客解码AI算法1 小时前
Windsurf :AI编程中 Agent 模式的先驱,有什么新东西?
ai编程
轻语呢喃2 小时前
用AI编程助手打造小游戏:从《谁是卧底》看Trae和Cursor的实战应用
cursor·trae
子昕2 小时前
解放双手!Cursor一键接管Chrome的MCP神器
ai编程
Captaincc6 小时前
Claude Code 如何在无索引机制下实现高效实时代码检索?
ai编程·claude
小磊哥er7 小时前
【前端AI实践】DeepSeek:开源大模型的使用让开发过程不再抓头发
前端·vue.js·ai编程
Kier7 小时前
基于YOLO的智能条码识别模型标注与训练
人工智能·python·ai编程
Baihai_IDP7 小时前
深度解析 Cursor(逐行解析系统提示词、分享高效制定 Cursor Rules 的技巧...)
人工智能·ai编程·cursor
踩着两条虫7 小时前
AI + 低代码 技术揭秘(十一):AI 集成
低代码·ai编程