欢迎光临
我们一直在努力

网页以及移动端自适应的个性化签名设计工具的实现

概述

随着数字时代的到来,电子签名在各行各业中的应用越来越广泛。今天我将分享一个功能完整的移动端优化签名设计工具,支持文本签名和手写签名,并适配所有设备。这个工具不仅美观实用,还包含了丰富的自定义功能。

功能亮点

核心功能

  • 文本签名:支持添加自定义文本签名,可调整大小、颜色、字体和旋转角度

  • 手写签名:提供画布手写签名功能,支持笔触大小调整

  • 移动端优化:完美适配手机和平板,支持触摸操作

  • 背景自定义:可上传背景图片并调整亮度

  • 签名管理:支持添加、删除、编辑和调整多个签名

  • 导出功能:一键导出设计结果为高质量PNG图片

  • 技术特性

    • 响应式设计,适配所有屏幕尺寸

    • 触摸友好的界面和操作

    • 使用html2canvas实现高质量截图导出

    • 纯前端实现,无需后端支持

    使用说明

    基础操作

  • 添加文本签名:点击"添加文本签名"按钮,在文本框中输入签名内容

  • 手写签名:点击"手写签名"按钮,使用鼠标或触摸屏绘制签名

  • 调整签名:拖动签名调整位置,拖动角落控制点调整大小

  • 修改属性:通过控制面板调整颜色、字体、大小和旋转角度

  • 保存结果:点击"下载签名设计"按钮导出最终设计

  • 移动端特别提示

    • 长按签名可拖动位置

    • 双指缩放可调整签名大小

    • 手写签名时,触摸绘制更自然

    技术实现详解

    1. 响应式设计

    /* 响应式断点 */
    @media (max-width: 768px) {
    .workspace {
    min-height: 350px;
    }
    .signature-text {
    font-size: 1.8rem;
    }
    }

    @media (max-width: 480px) {
    .workspace {
    min-height: 300px;
    }
    .signature-text {
    font-size: 1.6rem;
    }
    }

    2. 签名拖拽与缩放

    // 使元素可拖动(支持触摸事件)
    function makeDraggable(element) {
    element.onmousedown = dragMouseDown;
    element.ontouchstart = dragTouchStart;

    // 移动端触摸事件处理
    function dragTouchStart(e) {
    e.preventDefault();
    document.body.style.overflow = 'hidden'; // 防止拖动时页面滚动
    // … 其他触摸处理逻辑
    }
    }

    3. 手写签名实现

    // 手写签名绘制功能
    function startDrawing(e) {
    isDrawing = true;
    [lastX, lastY] = getCoordinates(e);
    }

    function draw(e) {
    if (!isDrawing) return;
    const [x, y] = getCoordinates(e);

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

    [lastX, lastY] = [x, y];
    }

    // 支持触摸事件
    handwritingCanvas.addEventListener('touchstart', startDrawing);
    handwritingCanvas.addEventListener('touchmove', draw);
    handwritingCanvas.addEventListener('touchend', stopDrawing);

    4. 图片导出功能

    // 使用html2canvas导出图片
    downloadBtn.addEventListener('click', function() {
    html2canvas(document.querySelector('.workspace'), {
    backgroundColor: null,
    scale: 2, // 提高导出质量
    useCORS: true
    }).then(canvas => {
    const link = document.createElement('a');
    link.download = 'signature-design.png';
    link.href = canvas.toDataURL('image/png');
    link.click();
    });
    });

    完整代码实现

    以下是完整的HTML代码,包含了所有CSS样式和JavaScript功能:

    <!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">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
    <style>
    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Microsoft YaHei', sans-serif;
    touch-action: manipulation;
    }

    body {
    background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
    min-height: 100vh;
    padding: 15px;
    display: flex;
    justify-content: center;
    align-items: center;
    overflow-x: hidden;
    }

    .container {
    width: 100%;
    max-width: 1400px;
    background: rgba(255, 255, 255, 0.95);
    border-radius: 20px;
    box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    }

    header {
    background: linear-gradient(90deg, #2c3e50, #4a6491);
    color: white;
    padding: 20px 15px;
    text-align: center;
    }

    h1 {
    font-size: 2.2rem;
    margin-bottom: 8px;
    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
    }

    .subtitle {
    font-size: 1.1rem;
    opacity: 0.9;
    }

    .main-content {
    display: flex;
    flex-wrap: wrap;
    padding: 15px;
    gap: 15px;
    }

    .workspace {
    flex: 1;
    min-width: 300px;
    min-height: 400px;
    background: linear-gradient(135deg, #e6f7ff, #f0f9ff);
    border: 2px dashed #aaa;
    border-radius: 10px;
    position: relative;
    overflow: hidden;
    box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.1);
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    }

    .background-canvas {
    width: 100%;
    height: 100%;
    position: relative;
    }

    .signature {
    position: absolute;
    cursor: move;
    user-select: none;
    min-width: 120px;
    min-height: 60px;
    border: 2px solid transparent;
    border-radius: 5px;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 15px;
    background: transparent;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
    transform-origin: center;
    resize: none;
    transition: border-color 0.3s ease;
    touch-action: none;
    }

    .signature.selected {
    border-color: #e74c3c;
    z-index: 10;
    }

    .signature-controls {
    position: absolute;
    top: -35px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    gap: 5px;
    background: rgba(255, 255, 255, 0.9);
    padding: 5px;
    border-radius: 20px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.2);
    opacity: 0;
    visibility: hidden;
    transition: all 0.3s ease;
    z-index: 100;
    }

    .signature.selected .signature-controls {
    opacity: 1;
    visibility: visible;
    }

    .signature-btn {
    background: #3498db;
    color: white;
    border: none;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    font-size: 12px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
    z-index: 10;
    transition: all 0.2s;
    }

    .signature-btn:hover {
    transform: scale(1.1);
    box-shadow: 0 3px 6px rgba(0,0,0,0.3);
    }

    .signature-btn.delete {
    background: #e74c3c;
    }

    .signature-btn.rotate {
    background: #9b59b6;
    }

    .signature-btn.edit {
    background: #f39c12;
    }

    .signature-text {
    font-size: 2.2rem;
    font-weight: solid;
    color: #000000;
    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
    cursor: text;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 5px;
    transition: all 0.3s ease;
    }

    .controls {
    width: 100%;
    max-width: 400px;
    background: #ecf0f1;
    border-radius: 10px;
    padding: 20px;
    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
    }

    .control-group {
    margin-bottom: 20px;
    padding-bottom: 15px;
    border-bottom: 1px solid #bdc3c7;
    }

    .control-group h3 {
    color: #2c3e50;
    margin-bottom: 12px;
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 1.2rem;
    }

    .btn {
    display: inline-block;
    width: auto;
    padding: 10px 15px;
    margin: 5px 5px 5px 0;
    background: #3498db;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-size: 0.9rem;
    font-weight: 600;
    transition: all 0.3s;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    }

    .btn:hover {
    background: #2980b9;
    transform: translateY(-3px);
    box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
    }

    .btn i {
    margin-right: 8px;
    }

    .btn.delete {
    background: #e74c3c;
    }

    .btn.delete:hover {
    background: #c0392b;
    }

    .btn.download {
    background: #27ae60;
    }

    .btn.download:hover {
    background: #219653;
    }

    .btn.handwriting {
    background: #9b59b6;
    }

    .btn.handwriting:hover {
    background: #8e44ad;
    }

    .btn.clear {
    background: #f39c12;
    }

    .btn.clear:hover {
    background: #d35400;
    }

    .slider-container {
    margin: 15px 0;
    }

    .slider-container label {
    display: block;
    margin-bottom: 8px;
    font-weight: 600;
    color: #34495e;
    font-size: 1rem;
    }

    .slider {
    width: 100%;
    height: 8px;
    -webkit-appearance: none;
    background: #bdc3c7;
    border-radius: 5px;
    outline: none;
    }

    .slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 20px;
    height: 20px;
    background: #3498db;
    border-radius: 50%;
    cursor: pointer;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
    transition: all 0.2s;
    }

    .slider::-webkit-slider-thumb:hover {
    transform: scale(1.1);
    background: #2980b9;
    }

    .color-picker {
    display: flex;
    gap: 10px;
    margin-top: 10px;
    flex-wrap: wrap;
    }

    .color-option {
    width: 30px;
    height: 30px;
    border-radius: 50%;
    cursor: pointer;
    border: 2px solid #fff;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
    transition: all 0.2s;
    }

    .color-option:hover {
    transform: scale(1.15);
    }

    .color-option.active {
    transform: scale(1.2);
    border: 2px solid #2c3e50;
    box-shadow: 0 0 0 2px white, 0 0 0 4px #2c3e50;
    }

    .font-select {
    width: 100%;
    padding: 10px;
    border-radius: 8px;
    border: 1px solid #bdc3c7;
    background: white;
    margin-top: 10px;
    font-size: 0.95rem;
    cursor: pointer;
    transition: all 0.2s;
    }

    .font-select:hover {
    border-color: #3498db;
    box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
    }

    .text-input {
    width: 100%;
    padding: 10px;
    border-radius: 8px;
    border: 1px solid #bdc3c7;
    background: white;
    margin-top: 10px;
    font-size: 1rem;
    transition: all 0.2s;
    }

    .text-input:focus {
    outline: none;
    border-color: #3498db;
    box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
    }

    .preview-container {
    background: #fff;
    border-radius: 10px;
    padding: 15px;
    margin-top: 20px;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
    }

    .preview-container h3 {
    color: #2c3e50;
    margin-bottom: 12px;
    font-size: 1.2rem;
    }

    .preview-box {
    height: 100px;
    background: #ecf0f1;
    border-radius: 8px;
    display: flex;
    justify-content: center;
    align-items: center;
    overflow: hidden;
    border: 1px solid #bdc3c7;
    }

    .instructions {
    background: #e3f2fd;
    padding: 15px;
    border-radius: 8px;
    margin-top: 20px;
    }

    .instructions h3 {
    color: #0d47a1;
    margin-bottom: 12px;
    font-size: 1.2rem;
    display: flex;
    align-items: center;
    gap: 8px;
    }

    .instructions ul {
    padding-left: 20px;
    }

    .instructions li {
    margin: 10px 0;
    color: #37474f;
    line-height: 1.5;
    font-size: 0.95rem;
    }

    .instructions li strong {
    color: #c0392b;
    font-weight: 600;
    }

    footer {
    text-align: center;
    padding: 15px;
    background: #2c3e50;
    color: #ecf0f1;
    font-size: 0.9rem;
    }

    .signature-editor {
    background: #fff;
    border-radius: 8px;
    padding: 15px;
    margin-top: 15px;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
    }

    .resize-handle {
    position: absolute;
    width: 16px;
    height: 16px;
    background: #3498db;
    border-radius: 50%;
    cursor: pointer;
    z-index: 5;
    box-shadow: 0 0 5px rgba(0,0,0,0.5);
    border: 2px solid white;
    opacity: 0;
    transition: opacity 0.3s ease;
    }

    .signature.selected .resize-handle {
    opacity: 1;
    }

    .resize-handle.tl {
    top: -8px;
    left: -8px;
    cursor: nw-resize;
    }

    .resize-handle.tr {
    top: -8px;
    right: -8px;
    cursor: ne-resize;
    }

    .resize-handle.bl {
    bottom: -8px;
    left: -8px;
    cursor: sw-resize;
    }

    .resize-handle.br {
    bottom: -8px;
    right: -8px;
    cursor: se-resize;
    }

    .font-preview {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 12px;
    }

    .font-sample {
    padding: 8px;
    border-radius: 6px;
    background: white;
    border: 1px solid #ddd;
    text-align: center;
    flex: 1;
    min-width: 100px;
    cursor: pointer;
    transition: all 0.2s;
    }

    .font-sample:hover {
    transform: translateY(-3px);
    box-shadow: 0 5px 10px rgba(0,0,0,0.1);
    border-color: #3498db;
    }

    .font-sample.active {
    border: 2px solid #3498db;
    background: #e3f2fd;
    transform: translateY(-3px);
    box-shadow: 0 5px 15px rgba(52, 152, 219, 0.3);
    }

    .font-sample h4 {
    font-size: 0.8rem;
    color: #7f8c8d;
    margin-bottom: 6px;
    }

    .font-sample p {
    font-size: 1.3rem;
    font-weight: bold;
    color: #000000;
    }

    /* 手写签名画布 */
    #handwritingCanvas {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    cursor: crosshair;
    display: none;
    z-index: 20;
    }

    .handwriting-controls {
    position: absolute;
    top: 15px;
    right: 15px;
    background: rgba(255, 255, 255, 0.9);
    padding: 10px;
    border-radius: 8px;
    box-shadow: 0 4px 10px rgba(0,0,0,0.2);
    display: none;
    z-index: 30;
    flex-direction: column;
    gap: 8px;
    max-width: 240px;
    }

    .handwriting-controls .btn {
    margin: 0;
    padding: 8px 12px;
    }

    .handwriting-mode .signature {
    opacity: 0.3;
    }

    .handwriting-mode .workspace {
    background-image: none;
    background-color: white;
    }

    .pen-size-control {
    display: flex;
    flex-direction: column;
    gap: 5px;
    }

    .pen-size-indicator {
    width: 30px;
    height: 30px;
    border-radius: 50%;
    background: #000;
    display: flex;
    justify-content: center;
    align-items: center;
    color: white;
    font-weight: bold;
    font-size: 12px;
    margin-top: 5px;
    align-self: center;
    }

    .handwriting-preview {
    position: absolute;
    bottom: 15px;
    left: 15px;
    background: rgba(255, 255, 255, 0.9);
    padding: 10px;
    border-radius: 8px;
    box-shadow: 0 4px 10px rgba(0,0,0,0.2);
    display: none;
    z-index: 30;
    width: 150px;
    height: 60px;
    border: 1px dashed #3498db;
    }

    .handwriting-preview img {
    max-width: 100%;
    max-height: 100%;
    display: block;
    margin: 0 auto;
    }

    /* 移动端提示 */
    .mobile-drag-hint {
    position: absolute;
    bottom: 15px;
    left: 0;
    right: 0;
    text-align: center;
    color: #3498db;
    font-weight: bold;
    background: rgba(255, 255, 255, 0.8);
    padding: 8px;
    border-radius: 5px;
    display: none;
    z-index: 10;
    animation: pulse 2s infinite;
    }

    @keyframes pulse {
    0% { opacity: 0.5; }
    50% { opacity: 1; }
    100% { opacity: 0.5; }
    }

    /* 响应式设计 */
    @media (max-width: 1200px) {
    .main-content {
    flex-direction: column;
    }

    .workspace {
    min-height: 500px;
    }

    .controls {
    max-width: 100%;
    }
    }

    @media (max-width: 768px) {
    .container {
    border-radius: 15px;
    }

    header {
    padding: 15px 10px;
    }

    h1 {
    font-size: 1.7rem;
    }

    .subtitle {
    font-size: 0.9rem;
    }

    .main-content {
    padding: 10px;
    gap: 10px;
    }

    .workspace {
    min-height: 350px;
    }

    .signature-text {
    font-size: 1.8rem;
    }

    .signature-controls {
    top: -30px;
    }

    .signature-btn {
    width: 24px;
    height: 24px;
    }

    .controls {
    padding: 15px;
    }

    .control-group {
    margin-bottom: 15px;
    padding-bottom: 12px;
    }

    .control-group h3 {
    font-size: 1.1rem;
    }

    .btn {
    padding: 8px 12px;
    font-size: 0.85rem;
    }

    .font-sample p {
    font-size: 1.2rem;
    }

    .preview-box {
    height: 90px;
    }

    .handwriting-controls {
    max-width: 180px;
    padding: 8px;
    top: 10px;
    right: 10px;
    }

    .handwriting-controls .btn {
    padding: 6px 10px;
    font-size: 0.8rem;
    }

    .pen-size-control {
    width: 100%;
    }

    .pen-size-control label {
    font-size: 0.9rem;
    }

    .pen-size-control input {
    width: 100%;
    }

    .mobile-drag-hint {
    display: block;
    }
    }

    @media (max-width: 480px) {
    body {
    padding: 10px;
    }

    .container {
    border-radius: 12px;
    }

    .workspace {
    min-height: 300px;
    }

    .signature-text {
    font-size: 1.6rem;
    }

    .signature-controls {
    top: -28px;
    }

    .signature-btn {
    width: 22px;
    height: 22px;
    font-size: 10px;
    }

    .controls {
    padding: 12px;
    }

    .font-select, .text-input {
    font-size: 0.9rem;
    padding: 8px;
    }

    .slider-container label {
    font-size: 0.95rem;
    }

    .color-option {
    width: 26px;
    height: 26px;
    }

    .font-preview {
    gap: 8px;
    }

    .font-sample {
    min-width: 80px;
    padding: 6px;
    }

    .font-sample p {
    font-size: 1.1rem;
    }

    .instructions li {
    font-size: 0.9rem;
    }

    .handwriting-controls {
    max-width: 160px;
    padding: 6px;
    top: 8px;
    right: 8px;
    }

    .handwriting-controls .btn {
    padding: 5px 8px;
    font-size: 0.75rem;
    }
    }

    @media (max-width: 360px) {
    h1 {
    font-size: 1.5rem;
    }

    .workspace {
    min-height: 250px;
    }

    .signature-text {
    font-size: 1.4rem;
    }

    .font-sample {
    min-width: 70px;
    }

    .font-sample p {
    font-size: 1rem;
    }
    }

    /* 中文字体定义 */
    .font-yahei { font-family: 'Microsoft YaHei', sans-serif; }
    .font-song { font-family: SimSun, serif; }
    .font-kai { font-family: KaiTi, cursive; }
    .font-hei { font-family: SimHei, sans-serif; }
    .font-youyuan { font-family: YouYuan, cursive; }
    .font-libian { font-family: LiSu, cursive; }
    .font-fangsong { font-family: FangSong, serif; }
    .font-xingkai { font-family: STXingkai, cursive; }
    </style>
    </head>
    <body>
    <div class="container">
    <header>
    <h1><i class="fas fa-signature"></i> 专业签名设计工具</h1>
    <p class="subtitle">支持文本与手写签名 – 完美适配所有设备</p>
    </header>

    <div class="main-content">
    <div class="workspace">
    <div class="mobile-drag-hint">提示:长按签名拖动位置</div>
    <div class="background-canvas" id="backgroundCanvas">
    <!– 签名元素将通过JavaScript动态添加 –>
    </div>
    <!– 手写签名画布 –>
    <canvas id="handwritingCanvas"></canvas>
    <!– 手写控制按钮 –>
    <div class="handwriting-controls">
    <div class="pen-size-control">
    <label>笔触大小:</label>
    <input type="range" min="1" max="20" value="5" id="penSizeSlider">
    <div class="pen-size-indicator" id="penSizeIndicator">5</div>
    </div>
    <button class="btn clear" id="clearHandwritingBtn">
    <i class="fas fa-eraser"></i> 清除
    </button>
    <button class="btn download" id="saveHandwritingBtn">
    <i class="fas fa-check"></i> 保存签名
    </button>
    <button class="btn delete" id="cancelHandwritingBtn">
    <i class="fas fa-times"></i> 取消
    </button>
    </div>
    <!– 手写预览 –>
    <div class="handwriting-preview" id="handwritingPreview"></div>
    </div>

    <div class="controls">
    <div class="control-group">
    <h3><i class="fas fa-image"></i> 背景设置</h3>
    <button class="btn" id="uploadBgBtn">
    <i class="fas fa-upload"></i> 上传背景图片
    </button>
    <input type="file" id="bgImageInput" accept="image/*" style="display: none;">

    <div class="slider-container">
    <label for="bgBrightness">背景亮度: <span id="brightnessValue">100%</span></label>
    <input type="range" min="0" max="200" value="100" class="slider" id="bgBrightness">
    </div>
    </div>

    <div class="control-group">
    <h3><i class="fas fa-signature"></i> 签名设置</h3>
    <div class="btn-group">
    <button class="btn" id="addSignatureBtn">
    <i class="fas fa-plus"></i> 添加文本签名
    </button>
    <button class="btn handwriting" id="addHandwritingBtn">
    <i class="fas fa-pen"></i> 手写签名
    </button>
    <button class="btn delete" id="removeSignatureBtn">
    <i class="fas fa-trash"></i> 删除当前签名
    </button>
    </div>

    <div class="signature-editor">
    <div class="slider-container">
    <label for="signatureText">签名文本:</label>
    <input type="text" id="signatureText" class="text-input" placeholder="输入签名内容" value="张伟">
    </div>

    <div class="slider-container">
    <label for="signatureSize">签名大小: <span id="sizeValue">100%</span></label>
    <input type="range" min="20" max="300" value="100" class="slider" id="signatureSize">
    </div>

    <div class="slider-container">
    <label for="signatureRotation">旋转角度: <span id="rotationValue">0°</span></label>
    <input type="range" min="0" max="360" value="0" class="slider" id="signatureRotation">
    </div>

    <label>签名颜色:</label>
    <div class="color-picker">
    <div class="color-option active" style="background-color: #000000;" data-color="#000000"></div>
    <div class="color-option" style="background-color: #2c3e50;" data-color="#2c3e50"></div>
    <div class="color-option" style="background-color: #c0392b;" data-color="#c0392b"></div>
    <div class="color-option" style="background-color: #27ae60;" data-color="#27ae60"></div>
    <div class="color-option" style="background-color: #8e44ad;" data-color="#8e44ad"></div>
    <div class="color-option" style="background-color: #2980b9;" data-color="#2980b9"></div>
    <div class="color-option" style="background-color: #d35400;" data-color="#d35400"></div>
    <div class="color-option" style="background-color: #16a085;" data-color="#16a085"></div>
    </div>

    <label>签名字体:</label>
    <select class="font-select" id="signatureFont">
    <option value="font-yahei">微软雅黑</option>
    <option value="font-song">宋体</option>
    <option value="font-kai">楷体</option>
    <option value="font-hei">黑体</option>
    <option value="font-youyuan">幼圆</option>
    <option value="font-libian">隶书</option>
    <option value="font-fangsong">仿宋</option>
    <option value="font-xingkai">行楷</option>
    </select>

    <div class="font-preview">
    <div class="font-sample active" data-font="font-yahei">
    <h4>微软雅黑</h4>
    <p class="font-yahei">张伟</p>
    </div>
    <div class="font-sample" data-font="font-song">
    <h4>宋体</h4>
    <p class="font-song">张伟</p>
    </div>
    <div class="font-sample" data-font="font-kai">
    <h4>楷体</h4>
    <p class="font-kai">张伟</p>
    </div>
    <div class="font-sample" data-font="font-hei">
    <h4>黑体</h4>
    <p class="font-hei">张伟</p>
    </div>
    </div>
    </div>
    </div>

    <div class="control-group">
    <h3><i class="fas fa-download"></i> 导出结果</h3>
    <button class="btn download" id="downloadBtn">
    <i class="fas fa-download"></i> 下载签名设计
    </button>
    </div>

    <div class="preview-container">
    <h3><i class="fas fa-eye"></i> 签名预览</h3>
    <div class="preview-box">
    <div class="signature-text" id="signaturePreview">张伟</div>
    </div>
    </div>

    <div class="instructions">
    <h3><i class="fas fa-info-circle"></i> 使用说明</h3>
    <ul>
    <li><strong>添加签名</strong>:点击"添加文本签名"或"手写签名"按钮</li>
    <li><strong>手写签名</strong>:使用鼠标/触摸屏绘制签名,完成后点击"保存签名"</li>
    <li><strong>编辑内容</strong>:在文本框中输入您的姓名</li>
    <li><strong>移动位置</strong>:拖动签名到任意位置</li>
    <li><strong>自由缩放</strong>:拖动签名四个角的蓝色控制点调整大小</li>
    <li><strong>字体选择</strong>:从下拉菜单选择或点击预览卡片切换字体</li>
    <li><strong>旋转签名</strong>:使用控制面板滑块或签名右上角旋转按钮</li>
    <li><strong>背景设置</strong>:上传您自己的背景图片</li>
    <li><strong>保存结果</strong>:完成后点击下载按钮</li>
    </ul>
    </div>
    </div>
    </div>

    <footer>
    <p>© 2026 专业签名设计工具 | 支持文本与手写签名</p>
    </footer>
    </div>

    <script>
    document.addEventListener('DOMContentLoaded', function() {
    // 元素引用
    const workspace = document.getElementById('backgroundCanvas');
    const uploadBtn = document.getElementById('uploadBgBtn');
    const bgInput = document.getElementById('bgImageInput');
    const addSignatureBtn = document.getElementById('addSignatureBtn');
    const addHandwritingBtn = document.getElementById('addHandwritingBtn');
    const removeSignatureBtn = document.getElementById('removeSignatureBtn');
    const sizeSlider = document.getElementById('signatureSize');
    const rotationSlider = document.getElementById('signatureRotation');
    const brightnessSlider = document.getElementById('bgBrightness');
    const colorOptions = document.querySelectorAll('.color-option');
    const fontSelect = document.getElementById('signatureFont');
    const downloadBtn = document.getElementById('downloadBtn');
    const previewBox = document.getElementById('signaturePreview');
    const textInput = document.getElementById('signatureText');
    const fontSamples = document.querySelectorAll('.font-sample');
    const handwritingCanvas = document.getElementById('handwritingCanvas');
    const clearHandwritingBtn = document.getElementById('clearHandwritingBtn');
    const saveHandwritingBtn = document.getElementById('saveHandwritingBtn');
    const cancelHandwritingBtn = document.getElementById('cancelHandwritingBtn');
    const handwritingControls = document.querySelector('.handwriting-controls');
    const penSizeSlider = document.getElementById('penSizeSlider');
    const penSizeIndicator = document.getElementById('penSizeIndicator');
    const handwritingPreview = document.getElementById('handwritingPreview');

    // 当前选中的签名
    let currentSignature = null;
    let signatureCount = 0;
    let originalSizes = new Map(); // 存储原始尺寸

    // 手写签名相关变量
    let isDrawing = false;
    let lastX = 0;
    let lastY = 0;
    let ctx = null;
    let penSize = 3; // 初始笔触大小

    // 初始化画布
    function initCanvas() {
    const rect = workspace.parentElement.getBoundingClientRect();
    handwritingCanvas.width = rect.width;
    handwritingCanvas.height = rect.height;

    ctx = handwritingCanvas.getContext('2d');
    ctx.lineWidth = penSize;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.strokeStyle = '#000000'; // 黑色笔触
    }

    // 笔触大小控制
    penSizeSlider.addEventListener('input', function() {
    penSize = this.value;
    ctx.lineWidth = penSize;
    penSizeIndicator.textContent = penSize;
    });

    // 背景设置 – 修复移动端上传问题
    uploadBtn.addEventListener('click', () => bgInput.click());

    bgInput.addEventListener('change', function(e) {
    if (e.target.files && e.target.files[0]) {
    const reader = new FileReader();

    reader.onload = function(event) {
    workspace.parentElement.style.backgroundImage = `url(${event.target.result})`;
    workspace.parentElement.style.backgroundSize = "cover";
    workspace.parentElement.style.backgroundRepeat = "no-repeat";
    workspace.parentElement.style.backgroundPosition = "center";
    }

    reader.onerror = function(error) {
    console.error('文件读取错误:', error);
    alert('背景图片上传失败,请重试');
    };

    reader.readAsDataURL(e.target.files[0]);
    }
    });

    brightnessSlider.addEventListener('input', function() {
    const value = this.value;
    document.getElementById('brightnessValue').textContent = `${value}%`;
    workspace.parentElement.style.filter = `brightness(${value}%)`;
    });

    // 签名创建和删除
    addSignatureBtn.addEventListener('click', function() {
    signatureCount++;
    const signature = document.createElement('div');
    signature.className = 'signature';
    signature.innerHTML = `
    <div class="signature-text font-yahei">${textInput.value || '签名' + signatureCount}</div>
    <div class="signature-controls">
    <button class="signature-btn edit" title="编辑"><i class="fas fa-edit"></i></button>
    <button class="signature-btn rotate" title="旋转"><i class="fas fa-sync-alt"></i></button>
    <button class="signature-btn delete" title="删除"><i class="fas fa-times"></i></button>
    </div>
    <div class="resize-handle tl" title="左上角缩放"></div>
    <div class="resize-handle tr" title="右上角缩放"></div>
    <div class="resize-handle bl" title="左下角缩放"></div>
    <div class="resize-handle br" title="右下角缩放"></div>
    `;

    // 初始位置在画布中央
    signature.style.left = '50%';
    signature.style.top = '50%';
    signature.style.transform = 'translate(-50%, -50%)';

    workspace.appendChild(signature);

    // 记录原始尺寸
    originalSizes.set(signature, {
    width: 180,
    height: 80
    });

    selectSignature(signature);

    // 添加拖动功能
    makeDraggable(signature);

    // 添加缩放功能
    addResizeFunctionality(signature);

    // 添加控制按钮事件
    const editBtn = signature.querySelector('.edit');
    const rotateBtn = signature.querySelector('.rotate');
    const deleteBtn = signature.querySelector('.delete');

    // 编辑按钮
    editBtn.addEventListener('click', function(e) {
    e.stopPropagation();
    const text = signature.querySelector('.signature-text').textContent;
    textInput.value = text;
    textInput.focus();
    });

    // 旋转按钮
    rotateBtn.addEventListener('click', function(e) {
    e.stopPropagation();
    const currentRotation = parseInt(signature.dataset.rotation || 0);
    const newRotation = (currentRotation + 45) % 360;
    signature.style.transform = `translate(-50%, -50%) rotate(${newRotation}deg)`;
    signature.dataset.rotation = newRotation;
    rotationSlider.value = newRotation;
    document.getElementById('rotationValue').textContent = `${newRotation}°`;
    });

    // 删除按钮
    deleteBtn.addEventListener('click', function(e) {
    e.stopPropagation();
    signature.remove();
    originalSizes.delete(signature);
    if (currentSignature === signature) {
    currentSignature = null;
    updateControlsState();
    }
    });
    });

    // 默认添加一个签名
    setTimeout(() => {
    addSignatureBtn.click();
    }, 500);

    // 手写签名功能
    addHandwritingBtn.addEventListener('click', function() {
    // 初始化画布
    initCanvas();

    // 显示画布和控制按钮
    handwritingCanvas.style.display = 'block';
    handwritingControls.style.display = 'flex';
    workspace.parentElement.classList.add('handwriting-mode');

    // 显示笔触大小
    penSizeIndicator.textContent = penSize;

    // 禁用其他控制
    document.querySelectorAll('.controls .btn').forEach(btn => {
    btn.disabled = true;
    });
    });

    // 清除手写内容
    clearHandwritingBtn.addEventListener('click', function() {
    ctx.clearRect(0, 0, handwritingCanvas.width, handwritingCanvas.height);
    handwritingPreview.style.display = 'none';
    });

    // 保存手写签名
    saveHandwritingBtn.addEventListener('click', function() {
    // 创建签名元素
    signatureCount++;
    const signature = document.createElement('div');
    signature.className = 'signature';

    // 裁剪签名并放大
    const croppedCanvas = cropCanvas(handwritingCanvas);
    const scaledCanvas = scaleCanvas(croppedCanvas, 2); // 放大2倍

    // 创建图像元素
    const img = document.createElement('img');
    img.src = scaledCanvas.toDataURL('image/png');
    img.style.width = '100%';
    img.style.height = '100%';
    img.style.objectFit = 'contain';
    img.style.display = 'block';

    signature.appendChild(img);
    signature.innerHTML += `
    <div class="signature-controls">
    <button class="signature-btn rotate" title="旋转"><i class="fas fa-sync-alt"></i></button>
    <button class="signature-btn delete" title="删除"><i class="fas fa-times"></i></button>
    </div>
    <div class="resize-handle tl" title="左上角缩放"></div>
    <div class="resize-handle tr" title="右上角缩放"></div>
    <div class="resize-handle bl" title="左下角缩放"></div>
    <div class="resize-handle br" title="右下角缩放"></div>
    `;

    // 设置初始位置和大小
    signature.style.left = '50%';
    signature.style.top = '50%';
    signature.style.transform = 'translate(-50%, -50%)';
    signature.style.width = '300px';
    signature.style.height = '150px';
    signature.style.background = 'transparent';
    signature.style.padding = '0';

    // 添加居中样式
    signature.style.display = 'flex';
    signature.style.justifyContent = 'center';
    signature.style.alignItems = 'center';

    workspace.appendChild(signature);

    // 记录原始尺寸
    originalSizes.set(signature, {
    width: 300,
    height: 150
    });

    selectSignature(signature);

    // 添加拖动功能
    makeDraggable(signature);

    // 添加缩放功能
    addResizeFunctionality(signature);

    // 添加控制按钮事件
    const rotateBtn = signature.querySelector('.rotate');
    const deleteBtn = signature.querySelector('.delete');

    // 旋转按钮
    rotateBtn.addEventListener('click', function(e) {
    e.stopPropagation();
    const currentRotation = parseInt(signature.dataset.rotation || 0);
    const newRotation = (currentRotation + 45) % 360;
    signature.style.transform = `translate(-50%, -50%) rotate(${newRotation}deg)`;
    signature.dataset.rotation = newRotation;
    rotationSlider.value = newRotation;
    document.getElementById('rotationValue').textContent = `${newRotation}°`;
    });

    // 删除按钮
    deleteBtn.addEventListener('click', function(e) {
    e.stopPropagation();
    signature.remove();
    originalSizes.delete(signature);
    if (currentSignature === signature) {
    currentSignature = null;
    updateControlsState();
    }
    });

    // 退出手写模式
    exitHandwritingMode();
    });

    // 裁剪画布(去除空白)
    function cropCanvas(canvas) {
    const ctx = canvas.getContext('2d');
    const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const data = pixels.data;

    let top = 0, bottom = 0, left = 0, right = 0;

    // 查找顶部边界
    for (let y = 0; y < canvas.height; y++) {
    for (let x = 0; x < canvas.width; x++) {
    const alpha = data[((canvas.width * y) + x) * 4 + 3];
    if (alpha > 0) {
    top = y;
    y = canvas.height;
    break;
    }
    }
    }

    // 查找底部边界
    for (let y = canvas.height – 1; y >= 0; y–) {
    for (let x = 0; x < canvas.width; x++) {
    const alpha = data[((canvas.width * y) + x) * 4 + 3];
    if (alpha > 0) {
    bottom = y;
    y = -1;
    break;
    }
    }
    }

    // 查找左侧边界
    for (let x = 0; x < canvas.width; x++) {
    for (let y = 0; y < canvas.height; y++) {
    const alpha = data[((canvas.width * y) + x) * 4 + 3];
    if (alpha > 0) {
    left = x;
    x = canvas.width;
    break;
    }
    }
    }

    // 查找右侧边界
    for (let x = canvas.width – 1; x >= 0; x–) {
    for (let y = 0; y < canvas.height; y++) {
    const alpha = data[((canvas.width * y) + x) * 4 + 3];
    if (alpha > 0) {
    right = x;
    x = -1;
    break;
    }
    }
    }

    // 添加一些边距
    const padding = 20;
    top = Math.max(0, top – padding);
    left = Math.max(0, left – padding);
    bottom = Math.min(canvas.height, bottom + padding);
    right = Math.min(canvas.width, right + padding);

    const width = right – left;
    const height = bottom – top;

    // 创建裁剪后的画布
    const croppedCanvas = document.createElement('canvas');
    croppedCanvas.width = width;
    croppedCanvas.height = height;
    const croppedCtx = croppedCanvas.getContext('2d');
    croppedCtx.drawImage(canvas, left, top, width, height, 0, 0, width, height);

    return croppedCanvas;
    }

    // 放大画布内容
    function scaleCanvas(canvas, scaleFactor) {
    const scaledCanvas = document.createElement('canvas');
    scaledCanvas.width = canvas.width * scaleFactor;
    scaledCanvas.height = canvas.height * scaleFactor;
    const scaledCtx = scaledCanvas.getContext('2d');
    scaledCtx.imageSmoothingEnabled = true;
    scaledCtx.imageSmoothingQuality = 'high';
    scaledCtx.drawImage(canvas, 0, 0, scaledCanvas.width, scaledCanvas.height);
    return scaledCanvas;
    }

    // 取消手写签名
    cancelHandwritingBtn.addEventListener('click', exitHandwritingMode);

    // 退出手写模式
    function exitHandwritingMode() {
    handwritingCanvas.style.display = 'none';
    handwritingControls.style.display = 'none';
    handwritingPreview.style.display = 'none';
    workspace.parentElement.classList.remove('handwriting-mode');

    // 清除画布
    ctx.clearRect(0, 0, handwritingCanvas.width, handwritingCanvas.height);

    // 启用其他控制
    document.querySelectorAll('.controls .btn').forEach(btn => {
    btn.disabled = false;
    });
    }

    // 手写签名绘制功能
    function startDrawing(e) {
    isDrawing = true;
    [lastX, lastY] = getCoordinates(e);
    }

    function draw(e) {
    if (!isDrawing) return;

    const [x, y] = getCoordinates(e);

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

    [lastX, lastY] = [x, y];

    // 更新预览
    updateHandwritingPreview();
    }

    function stopDrawing() {
    isDrawing = false;
    updateHandwritingPreview();
    }

    // 更新手写预览
    function updateHandwritingPreview() {
    const croppedCanvas = cropCanvas(handwritingCanvas);
    const scaledCanvas = scaleCanvas(croppedCanvas, 1.5); // 预览时放大1.5倍

    handwritingPreview.innerHTML = '';
    const img = document.createElement('img');
    img.src = scaledCanvas.toDataURL('image/png');
    img.style.maxWidth = '100%';
    img.style.maxHeight = '100%';
    handwritingPreview.appendChild(img);
    handwritingPreview.style.display = 'block';
    }

    // 获取坐标(支持触摸事件)
    function getCoordinates(e) {
    let x, y;
    const rect = handwritingCanvas.getBoundingClientRect();

    if (e.type.includes('touch')) {
    const touch = e.touches[0] || e.changedTouches[0];
    x = touch.clientX – rect.left;
    y = touch.clientY – rect.top;
    } else {
    x = e.clientX – rect.left;
    y = e.clientY – rect.top;
    }

    return [x, y];
    }

    // 添加绘图事件监听器
    handwritingCanvas.addEventListener('mousedown', startDrawing);
    handwritingCanvas.addEventListener('mousemove', draw);
    handwritingCanvas.addEventListener('mouseup', stopDrawing);
    handwritingCanvas.addEventListener('mouseout', stopDrawing);

    // 触摸事件支持
    handwritingCanvas.addEventListener('touchstart', startDrawing);
    handwritingCanvas.addEventListener('touchmove', draw);
    handwritingCanvas.addEventListener('touchend', stopDrawing);

    removeSignatureBtn.addEventListener('click', function() {
    if (currentSignature) {
    currentSignature.remove();
    originalSizes.delete(currentSignature);
    currentSignature = null;
    updateControlsState();
    }
    });

    // 签名选择
    function selectSignature(signature) {
    // 移除之前选中的签名边框
    document.querySelectorAll('.signature').forEach(sig => {
    sig.classList.remove('selected');
    });

    // 设置当前签名
    if (signature) {
    signature.classList.add('selected');
    currentSignature = signature;

    // 更新控制面板的值
    const text = signature.querySelector('.signature-text')?.textContent || '';
    const size = parseInt(signature.dataset.size || 100);
    const rotation = parseInt(signature.dataset.rotation || 0);
    const color = signature.dataset.color || '#000000';
    const fontClass = signature.querySelector('.signature-text')?.className.split(' ').find(cls => cls.startsWith('font-')) || 'font-yahei';

    textInput.value = text;
    sizeSlider.value = size;
    document.getElementById('sizeValue').textContent = `${size}%`;

    rotationSlider.value = rotation;
    document.getElementById('rotationValue').textContent = `${rotation}°`;

    // 更新颜色选择器
    colorOptions.forEach(option => {
    option.classList.remove('active');
    if (option.dataset.color === color) {
    option.classList.add('active');
    }
    });

    // 更新字体选择器
    fontSelect.value = fontClass;

    // 更新字体预览卡片
    fontSamples.forEach(sample => {
    sample.classList.remove('active');
    if (sample.dataset.font === fontClass) {
    sample.classList.add('active');
    }
    });
    } else {
    currentSignature = null;
    }

    // 更新控件状态
    updateControlsState();
    }

    // 更新控件状态
    function updateControlsState() {
    const disabled = currentSignature === null;
    sizeSlider.disabled = disabled;
    rotationSlider.disabled = disabled;
    textInput.disabled = disabled;
    fontSelect.disabled = disabled;

    // 更新预览
    if (!disabled) {
    const signatureText = currentSignature.querySelector('.signature-text');
    if (signatureText) {
    previewBox.textContent = signatureText.textContent;
    previewBox.style.color = signatureText.style.color || '#000000';
    previewBox.className = "signature-text " + signatureText.className;
    previewBox.style.fontSize = signatureText.style.fontSize || "2rem";
    } else {
    // 手写签名没有文本元素
    previewBox.innerHTML = '<span style="color:#7f8c8d; font-size:1rem;">手写签名预览</span>';
    }
    } else {
    previewBox.textContent = textInput.value || "您的签名";
    previewBox.className = "signature-text font-yahei";
    previewBox.style.color = '#000000';
    }
    }

    // 使元素可拖动 – 修复移动端拖动问题
    function makeDraggable(element) {
    let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
    let touchOffsetX = 0, touchOffsetY = 0;

    element.onmousedown = dragMouseDown;
    element.ontouchstart = dragTouchStart;

    function dragMouseDown(e) {
    // 如果点击的是缩放控制点,则不触发拖动
    if (e.target.classList.contains('resize-handle')) return;

    e = e || window.event;
    e.preventDefault();

    // 选择当前签名
    selectSignature(element);

    // 获取鼠标位置
    pos3 = e.clientX;
    pos4 = e.clientY;
    document.onmouseup = closeDragElement;
    document.onmousemove = elementDrag;
    }

    function dragTouchStart(e) {
    if (e.target.classList.contains('resize-handle')) return;

    e.preventDefault();
    document.body.style.overflow = 'hidden'; // 防止拖动时页面滚动

    selectSignature(element);

    const touch = e.touches[0];
    const rect = element.getBoundingClientRect();

    // 计算触摸点相对于元素左上角的偏移
    touchOffsetX = touch.clientX – rect.left;
    touchOffsetY = touch.clientY – rect.top;

    pos3 = touch.clientX;
    pos4 = touch.clientY;
    document.ontouchend = closeDragElement;
    document.ontouchmove = elementTouchMove;
    }

    function elementDrag(e) {
    e = e || window.event;
    e.preventDefault();

    // 计算新位置
    pos1 = pos3 – e.clientX;
    pos2 = pos4 – e.clientY;
    pos3 = e.clientX;
    pos4 = e.clientY;

    // 设置元素新位置
    const rect = workspace.getBoundingClientRect();
    const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;

    // 计算新位置(像素单位)
    let newLeft = element.offsetLeft – pos1;
    let newTop = element.offsetTop – pos2;

    // 限制在工作区内
    newLeft = Math.max(0, Math.min(rect.width – element.offsetWidth, newLeft));
    newTop = Math.max(0, Math.min(rect.height – element.offsetHeight, newTop));

    element.style.left = `${newLeft}px`;
    element.style.top = `${newTop}px`;
    }

    // 修复移动端垂直拖动问题
    function elementTouchMove(e) {
    e.preventDefault();

    const touch = e.touches[0];
    const workspaceRect = workspace.parentElement.getBoundingClientRect();

    // 计算元素的新位置(相对于工作区的左上角)
    let newLeft = (touch.clientX – workspaceRect.left) – touchOffsetX;
    let newTop = (touch.clientY – workspaceRect.top) – touchOffsetY;

    // 限制在工作区内
    newLeft = Math.max(0, Math.min(workspaceRect.width – element.offsetWidth, newLeft));
    newTop = Math.max(0, Math.min(workspaceRect.height – element.offsetHeight, newTop));

    element.style.left = `${newLeft}px`;
    element.style.top = `${newTop}px`;
    }

    function closeDragElement() {
    // 恢复页面滚动
    document.body.style.overflow = '';
    document.onmouseup = null;
    document.onmousemove = null;
    document.ontouchend = null;
    document.ontouchmove = null;
    }
    }

    // 添加缩放功能(四个角)
    function addResizeFunctionality(element) {
    const resizeHandles = element.querySelectorAll('.resize-handle');

    resizeHandles.forEach(handle => {
    handle.addEventListener('mousedown', initResize);
    handle.addEventListener('touchstart', initResize);
    });

    function initResize(e) {
    e.preventDefault();
    e.stopPropagation();

    // 选择当前元素
    selectSignature(element);

    const handleClass = e.target.className.split(' ')[1]; // tl, tr, bl, br
    let startX, startY;
    const startLeft = element.offsetLeft;
    const startTop = element.offsetTop;
    const startWidth = element.offsetWidth;
    const startHeight = element.offsetHeight;

    if (e.type === 'mousedown') {
    startX = e.clientX;
    startY = e.clientY;
    } else if (e.type === 'touchstart') {
    startX = e.touches[0].clientX;
    startY = e.touches[0].clientY;
    }

    // 添加事件监听
    document.addEventListener('mousemove', resize);
    document.addEventListener('mouseup', stopResize);
    document.addEventListener('touchmove', resize);
    document.addEventListener('touchend', stopResize);

    function resize(e) {
    e.preventDefault();

    let currentX, currentY;

    if (e.type === 'mousemove') {
    currentX = e.clientX;
    currentY = e.clientY;
    } else if (e.type === 'touchmove') {
    currentX = e.touches[0].clientX;
    currentY = e.touches[0].clientY;
    }

    const dx = currentX – startX;
    const dy = currentY – startY;

    let newWidth = startWidth;
    let newHeight = startHeight;
    let newLeft = startLeft;
    let newTop = startTop;

    // 根据不同的控制点计算新位置和大小
    if (handleClass === 'br') {
    newWidth = Math.max(100, startWidth + dx);
    newHeight = Math.max(50, startHeight + dy);
    } else if (handleClass === 'bl') {
    newWidth = Math.max(100, startWidth – dx);
    newHeight = Math.max(50, startHeight + dy);
    newLeft = startLeft + dx;
    } else if (handleClass === 'tr') {
    newWidth = Math.max(100, startWidth + dx);
    newHeight = Math.max(50, startHeight – dy);
    newTop = startTop + dy;
    } else if (handleClass === 'tl') {
    newWidth = Math.max(100, startWidth – dx);
    newHeight = Math.max(50, startHeight – dy);
    newLeft = startLeft + dx;
    newTop = startTop + dy;
    }

    // 设置新位置和大小
    element.style.width = `${newWidth}px`;
    element.style.height = `${newHeight}px`;
    element.style.left = `${newLeft}px`;
    element.style.top = `${newTop}px`;

    // 更新控制面板的滑块值
    const sizeValue = Math.min(300, Math.max(20, Math.round((newWidth / originalSizes.get(element).width) * 100)));
    sizeSlider.value = sizeValue;
    document.getElementById('sizeValue').textContent = `${sizeValue}%`;

    // 更新字体大小(保持比例)
    const signatureText = element.querySelector('.signature-text');
    if (signatureText) {
    const fontSize = Math.max(1.2, Math.min(5, (newWidth / originalSizes.get(element).width) * 2));
    signatureText.style.fontSize = `${fontSize}rem`;
    }
    }

    function stopResize() {
    document.removeEventListener('mousemove', resize);
    document.removeEventListener('mouseup', stopResize);
    document.removeEventListener('touchmove', resize);
    document.removeEventListener('touchend', stopResize);
    }
    }
    }

    // 签名文本编辑
    textInput.addEventListener('input', function() {
    if (currentSignature) {
    const signatureText = currentSignature.querySelector('.signature-text');
    if (signatureText) {
    signatureText.textContent = this.value;
    // 更新预览
    previewBox.textContent = this.value;
    }
    } else {
    previewBox.textContent = this.value;
    }
    });

    // 签名属性控制 – 大小滑块
    sizeSlider.addEventListener('input', function() {
    const value = this.value;
    document.getElementById('sizeValue').textContent = `${value}%`;

    if (currentSignature) {
    const scale = value / 100;
    const originalSize = originalSizes.get(currentSignature);

    // 计算新尺寸
    const newWidth = originalSize.width * scale;
    const newHeight = originalSize.height * scale;

    // 设置新尺寸
    currentSignature.style.width = `${newWidth}px`;
    currentSignature.style.height = `${newHeight}px`;

    // 更新字体大小(保持比例)
    const signatureText = currentSignature.querySelector('.signature-text');
    if (signatureText) {
    const fontSize = Math.max(1.2, Math.min(5, scale * 2));
    signatureText.style.fontSize = `${fontSize}rem`;
    previewBox.style.fontSize = `${fontSize}rem`;
    }
    } else {
    previewBox.style.fontSize = `${value / 50}rem`;
    }
    });

    // 旋转滑块
    rotationSlider.addEventListener('input', function() {
    const value = this.value;
    document.getElementById('rotationValue').textContent = `${value}°`;

    if (currentSignature) {
    currentSignature.style.transform = `translate(-50%, -50%) rotate(${value}deg)`;
    currentSignature.dataset.rotation = value;
    }
    });

    // 颜色选择
    colorOptions.forEach(option => {
    option.addEventListener('click', function() {
    colorOptions.forEach(opt => opt.classList.remove('active'));
    this.classList.add('active');

    const color = this.dataset.color;
    if (currentSignature) {
    const signatureText = currentSignature.querySelector('.signature-text');
    if (signatureText) {
    signatureText.style.color = color;
    currentSignature.dataset.color = color;
    // 更新预览
    previewBox.style.color = color;
    }
    } else {
    previewBox.style.color = color;
    }
    });
    });

    // 字体选择
    fontSelect.addEventListener('change', function() {
    const fontClass = this.value;
    if (currentSignature) {
    const signatureText = currentSignature.querySelector('.signature-text');
    if (signatureText) {
    // 移除所有字体类
    signatureText.className = signatureText.className.replace(/\\bfont-\\w+\\b/g, '');
    // 添加新字体类
    signatureText.classList.add(fontClass);

    // 更新字体预览卡片
    fontSamples.forEach(sample => {
    sample.classList.remove('active');
    if (sample.dataset.font === fontClass) {
    sample.classList.add('active');
    }
    });

    // 更新预览
    previewBox.className = "signature-text " + fontClass;
    }
    } else {
    previewBox.className = "signature-text " + fontClass;
    }
    });

    // 字体预览卡片点击事件
    fontSamples.forEach(sample => {
    sample.addEventListener('click', function() {
    const fontClass = this.dataset.font;
    fontSelect.value = fontClass;

    // 更新字体预览卡片
    fontSamples.forEach(s => s.classList.remove('active'));
    this.classList.add('active');

    if (currentSignature) {
    const signatureText = currentSignature.querySelector('.signature-text');
    if (signatureText) {
    // 移除所有字体类
    signatureText.className = signatureText.className.replace(/\\bfont-\\w+\\b/g, '');
    // 添加新字体类
    signatureText.classList.add(fontClass);

    // 更新预览
    previewBox.className = "signature-text " + fontClass;
    }
    } else {
    previewBox.className = "signature-text " + fontClass;
    }
    });
    });

    // 下载功能 – 修复下载问题
    downloadBtn.addEventListener('click', function() {
    // 保存当前选中的签名状态
    const prevSelected = currentSignature;

    // 移除所有签名元素的选中状态(隐藏按钮和边框)
    document.querySelectorAll('.signature').forEach(sig => {
    sig.classList.remove('selected');
    });

    // 保存所有签名元素的原始状态
    const signatures = document.querySelectorAll('.workspace .signature');
    const originalStates = [];

    signatures.forEach(signature => {
    // 保存原始状态
    originalStates.push({
    element: signature,
    controls: signature.querySelector('.signature-controls')?.outerHTML || '',
    handles: Array.from(signature.querySelectorAll('.resize-handle')).map(h => h.outerHTML).join(''),
    border: signature.style.border,
    boxShadow: signature.style.boxShadow
    });

    // 临时移除控制按钮
    const controls = signature.querySelector('.signature-controls');
    if (controls) controls.remove();

    // 临时移除缩放点
    const resizeHandles = signature.querySelectorAll('.resize-handle');
    resizeHandles.forEach(handle => handle.remove());

    // 临时移除边框和阴影
    signature.style.border = 'none';
    signature.style.boxShadow = 'none';
    });

    // 截图
    setTimeout(() => {
    html2canvas(document.querySelector('.workspace'), {
    backgroundColor: null,
    scale: 2,
    useCORS: true,
    logging: true
    }).then(canvas => {
    // 创建下载链接
    const link = document.createElement('a');
    link.download = 'signature-design.png';
    link.href = canvas.toDataURL('image/png');
    link.click();

    // 恢复签名元素的原始状态
    originalStates.forEach(state => {
    state.element.style.border = state.border;
    state.element.style.boxShadow = state.boxShadow;

    // 重新添加控制按钮
    if (state.controls) {
    state.element.insertAdjacentHTML('beforeend', state.controls);
    }

    // 重新添加缩放点
    if (state.handles) {
    state.element.insertAdjacentHTML('beforeend', state.handles);
    }
    });

    // 恢复之前的选中状态
    if (prevSelected) {
    prevSelected.classList.add('selected');
    currentSignature = prevSelected;
    }
    }).catch(error => {
    console.error('截图失败:', error);

    // 恢复签名元素的原始状态
    originalStates.forEach(state => {
    state.element.style.border = state.border;
    state.element.style.boxShadow = state.boxShadow;

    if (state.controls) {
    state.element.insertAdjacentHTML('beforeend', state.controls);
    }

    if (state.handles) {
    state.element.insertAdjacentHTML('beforeend', state.handles);
    }
    });

    if (prevSelected) {
    prevSelected.classList.add('selected');
    currentSignature = prevSelected;
    }
    });
    }, 100); // 给DOM更新一点时间
    });

    // 点击文档其他区域取消选中签名
    document.addEventListener('click', function(e) {
    // 如果点击的不是签名元素或签名控制按钮
    if (!e.target.closest('.signature') && !e.target.closest('.signature-controls')) {
    selectSignature(null);
    }
    });

    // 初始状态更新
    updateControlsState();

    // 响应窗口大小变化
    window.addEventListener('resize', function() {
    if (handwritingCanvas.style.display === 'block') {
    initCanvas();
    }
    });
    });
    </script>
    </body>
    </html>

    部署与使用建议

    1. 本地部署

    • 将完整代码保存为HTML文件

    • 在浏览器中直接打开即可使用

    • 无需任何服务器环境

    2. 服务器部署

    • 上传HTML文件到任意Web服务器

    • 确保html2canvas库能正常加载

    • 配置正确的MIME类型

    3. 性能优化建议

    • 对于大量签名元素,考虑使用虚拟滚动

    • 手写签名画布可以优化为按需渲染

    • 导出图片时可添加加载提示

    常见问题解决

    1. 移动端触摸问题

    问题:在iOS设备上,触摸绘制不流畅 解决:添加touch-action: manipulation样式,优化触摸事件处理

    2. 导出图片模糊

    解决:在html2canvas配置中设置scale: 2提高分辨率

    3. 手写签名保存问题

    解决:添加签名裁剪和放大功能,确保手写签名清晰

    扩展功能建议

  • 云存储:添加将设计保存到云端的功能

  • 模板系统:预置多种签名模板

  • AI签名生成:集成AI生成艺术签名

  • 历史记录:保存用户的设计历史

  • 社交分享:一键分享到社交媒体

  • 总结

    这个签名设计工具是一个功能完整、移动端优化的前端应用,包含了文本签名和手写签名的所有基本功能。通过响应式设计和触摸友好的界面,它能够在各种设备上提供良好的用户体验。代码结构清晰,易于扩展和维护,是学习前端开发特别是Canvas操作和移动端优化的优秀示例。

    希望这个工具对你有所帮助!如果你有任何问题或改进建议,欢迎在评论区讨论。

    赞(0)
    未经允许不得转载:171主机测评 » 网页以及移动端自适应的个性化签名设计工具的实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址