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>
相关推荐
counterxing17 小时前
最近发现一个 Mac 工具,有点像把 Raycast、语音输入法、截图和录屏塞到了一起
macos·ai编程·claude
薛定喵的谔17 小时前
Term Proxy — 用 Tauri 2 打造跨平台终端配置管理工具
electron·ai编程·全栈
小溪彼岸17 小时前
CC Switch可视化管理Skill、提示词、会话
aigc·ai编程
豆包MarsCode18 小时前
Loop Engineering 到底是什么?看这一篇就够了
trae
aqi0020 小时前
15天学会AI应用开发(九)利用Chroma持久化向量数据
人工智能·python·大模型·ai编程·ai应用
kfaino1 天前
你好,我叫 Prompt——其实,你一直在给 AI 写程序
后端·openai·ai编程
kfaino1 天前
你好,我叫Token——AI世界里最忙的搬砖工
aigc·openai·ai编程
程序员老刘1 天前
Flutter版本选择指南:3.44系列继续观望 | 2026年6月
flutter·ai编程·客户端
洞窝技术2 天前
构建 AI 增量代码审查系统:AST 语义分析 + 多层约束架构 + LLM 多模型调度的工程实践
ai编程