电子手写签批h5架构设计

原生与h5对比

维度H5 手写签批原生 App 手写签批
压感与笔锋✅ 支持(Apple Pencil / Surface Pen / 三星 S Pen 均可通过浏览器 API 获取压感)✅ 支持,且延迟更低
延迟(跟手性)⚠️ 略高 5-15ms(复杂页面明显)✅ 更低,接近零延迟
离线可用❌ 需网络加载页面(可 PWA 缓存)✅ 完全离线
文件导出✅ PDF/PNG/SVG 均可✅ 更灵活,可直接写入系统相册/文件系统
多平台统一✅ 一套代码跑 iOS/Android/PC❌ iOS/Android 需分别开发
硬件适配⚠️ 依赖浏览器对触控笔的支持✅ 直接调用系统驱动,支持更全面

一、整体架构

┌─────────────────────────────────────────┐
│           前端层(PC + H5)               │
│  ┌─────────────┐    ┌─────────────┐     │
│  │   PC HTML   │    │   移动 H5    │     │
│  │  (大屏精细)  │    │  (触控优化)   │     │
│  └──────┬──────┘    └──────┬──────┘     │
│         └────────┬─────────┘            │
│              共用核心引擎                 │
│    ┌─────────────────────────┐          │
│    │  Perfect Freehand       │  ← 笔迹   │
│    │  PDF.js + 自定义渲染层    │  ← 文档   │
│    │  PinchZoom + Pan        │  ← 手势   │
│    └─────────────────────────┘          │
└─────────────────────────────────────────┘
                    │
┌─────────────────────────────────────────┐
│           服务层(Node/Java)             │
│    ┌─────────────────────────┐          │
│    │  笔迹数据存储(JSON)      │          │
│    │  PDF/图片合成导出         │          │
│    │  WebSocket 实时同步       │         │
│    └─────────────────────────┘          │
└─────────────────────────────────────────┘

二、核心技术选型

层级技术作用
笔迹引擎Perfect Freehand + 自研压感插值矢量笔迹、压感笔锋、丝滑曲线
文档渲染PDF.js(PDF)/ OpenSeadragon(大图)A4 精准定位、缩放、翻页
手势控制Hammer.js单指书写、双指缩放平移、防误触
跨端框架Vue3 + Vite(单套代码)PC 鼠标 + 移动触控统一适配
笔迹存储JSON 序列化(坐标+压感+时间戳)可回放、可编辑、可合并
导出合成pdf-lib.js(客户端)/ Serverless Puppeteer(服务端)PDF 签批层合并、图片叠加
实时同步Socket.io多人圈阅、领导代签、批注同步

三、关键设计细节

1. 丝滑不断触(核心难点)

问题根源:浏览器默认 touch 行为(滚动、缩放、长按菜单)会中断 pointermove 事件。

解决方案

/* 全局禁用默认触控行为 */
.signature-canvas {
  touch-action: none;           /* 禁止浏览器滚动/缩放 */
  user-select: none;            /* 禁止选中文本 */
  -webkit-touch-callout: none; /* 禁止 iOS 长按菜单 */
  overscroll-behavior: none;    /* 禁止弹性滚动 */
}
// JS 层彻底接管指针事件
canvas.addEventListener('pointerdown', (e) => {
  e.preventDefault();           // 阻断默认行为
  this.strokeStart(e);        // 开始笔迹
}, { passive: false });

canvas.addEventListener('pointermove', (e) => {
  e.preventDefault();
  if (e.getCoalescedEvents) {
    // Chrome 83+:获取合并事件,补全快速移动丢点
    const events = e.getCoalescedEvents();
    events.forEach(evt => this.strokeMove(evt));
  } else {
    this.strokeMove(e);
  }
}, { passive: false });

// 离开画布自动结束,防止断触残留
canvas.addEventListener('pointerup', this.strokeEnd);
canvas.addEventListener('pointercancel', this.strokeEnd);
canvas.addEventListener('pointerleave', this.strokeEnd);

进阶:预测绘制降低延迟

// 基于速度向量预测下一个点,提前渲染
function predictPoint(last, current) {
  const vx = current.x - last.x;
  const vy = current.y - last.y;
  return {
    x: current.x + vx * 0.3,  // 预测 30% 步长
    y: current.y + vy * 0.3,
    pressure: current.pressure,
    predicted: true            // 标记为预测点,收到真实点后替换
  };
}

2. 高级笔锋效果(自定义粗细、颜色、笔型)

数据结构

interface StrokePoint {
  x: number;
  y: number;
  pressure: number;      // 0-1,硬件压感或速度模拟
  timestamp: number;     // 用于回放速度计算
  tiltX?: number;       // Apple Pencil 倾斜
  tiltY?: number;
}

interface PenConfig {
  type: 'ballpen' | 'brush' | 'fountain' | 'marker';
  baseSize: number;      // 基础粗细 px
  minSize: number;       // 最细
  maxSize: number;       // 最粗
  color: string;         // #RRGGBB
  thinning: number;      // 0-1,速度变细系数
  smoothing: number;     // 0-1,平滑度
  streamline: number;    // 0-1,流线型
}

笔型渲染差异

笔型算法特征
圆珠笔压感变化小,边缘硬,适合公文批注
毛笔压感敏感,起收笔圆润,适合领导签名
钢笔倾斜影响扁度,模拟铱粒书写感
荧光笔半透明叠加,宽扁笔触,适合圈阅

Perfect Freehand 配置示例

import { getStroke } from 'perfect-freehand';

// 毛笔配置
const brushStroke = getStroke(points, {
  size: 12,
  thinning: 0.8,        // 速度快时明显变细
  smoothing: 0.6,       // 中等平滑
  streamline: 0.7,      // 笔迹流线
  easing: (t) => t * t, // 自定义缓动
  start: { cap: true, taper: 0 },   // 起笔圆头
  end: { cap: true, taper: 0 },     // 收笔圆头
});

// 圆珠笔配置(公文标准)
const penStroke = getStroke(points, {
  size: 2,
  thinning: 0.2,        // 几乎不变细
  smoothing: 0.5,
  streamline: 0.4,
  start: { cap: false, taper: 0 },  // 起笔平头
  end: { cap: false, taper: 0 },    // 收笔平头
});

3. A4 格式精准定位

坐标系设计

PDF/图片原始尺寸:595 × 842 pt(A4 72dpi)
渲染到屏幕:按容器宽度等比缩放
笔迹坐标:存储为百分比(0-1),适配任意分辨率

存储结构:
{
  "pageWidth": 595,
  "pageHeight": 842,
  "strokes": [
    {
      "pen": { "type": "ballpen", "size": 2, "color": "#ff0000" },
      "points": [[0.15, 0.23, 0.8], [0.152, 0.235, 0.9], ...]
      // [x%, y%, pressure]
    }
  ]
}

渲染层叠加

<div class="page-container" style="position: relative; width: 100%; aspect-ratio: 595/842;">
  <!-- 底稿层:PDF.js 渲染或图片 -->
  <canvas class="pdf-layer" style="position: absolute; top: 0; left: 0;"></canvas>
  
  <!-- 签批层:透明,接收触控 -->
  <canvas class="annotation-layer" 
          style="position: absolute; top: 0; left: 0; touch-action: none;">
  </canvas>
  
  <!-- 工具层:缩放、翻页、批注列表 -->
  <div class="toolbar">...</div>
</div>

4. PC + H5 统一适配

交互PC(鼠标)H5(触控)
书写左键按下拖动单指 touch,双指被 Hammer.js 拦截
缩放滚轮 / Ctrl+滚轮双指捏合
平移中键 / 空格+左键双指滑动
橡皮右键擦除 / 工具栏切换长按切换笔/擦,或侧边栏按钮
撤销Ctrl+Z三指左滑 / 悬浮按钮

Hammer.js 手势隔离

const mc = new Hammer(canvas);
mc.get('pinch').set({ enable: true });
mc.get('pan').set({ direction: Hammer.DIRECTION_ALL });

// 单指 = 书写(不触发 pan)
// 双指 = 缩放/平移
let pointers = 0;
canvas.addEventListener('pointerdown', (e) => {
  pointers++;
  if (pointers === 1) {
    mode = 'draw';      // 单指:书写
  } else {
    mode = 'gesture';   // 多指:手势
    currentStroke?.end(); // 中断当前笔迹
  }
});

5. 导出与归档

客户端合成(轻量)

import { PDFDocument } from 'pdf-lib';

async function mergeAnnotation(pdfBytes, strokesData) {
  const pdfDoc = await PDFDocument.load(pdfBytes);
  const page = pdfDoc.getPage(0);
  const { width, height } = page.getSize();
  
  // 笔迹转 SVG Path
  const svgPaths = strokesData.map(stroke => {
    const path = pointsToSVGPath(stroke.points, width, height);
    return { path, color: stroke.pen.color, width: stroke.pen.size };
  });
  
  // 绘制到 PDF
  svgPaths.forEach(({ path, color, width }) => {
    page.drawSvgPath(path, {
      borderColor: hexToRGB(color),
      borderWidth: width,
    });
  });
  
  return await pdfDoc.save();
}

服务端合成(高清打印)

// Node.js + Puppeteer + Canvas
const page = await browser.newPage();
await page.setViewport({ width: 794, height: 1123, deviceScaleFactor: 2 }); // 150dpi

await page.evaluate((strokesData) => {
  // 前端相同渲染逻辑,服务端执行
  renderStrokes(strokesData);
}, strokesData);

const pdf = await page.pdf({ format: 'A4', printBackground: true });

四、推荐开源组合

功能版本
笔迹引擎perfect-freehand^1.2
笔迹插值自研(速度模拟压感)-
PDF 渲染pdfjs-dist^4.0
PDF 编辑pdf-lib^1.17
手势控制hammerjs^2.0
前端框架vue^3.4
构建工具vite^5.0
状态管理pinia^2.0
实时同步socket.io-client^4.7

五、性能指标目标

指标目标值
首屏渲染< 1.5s(A4 PDF)
书写延迟< 16ms(60fps)
笔迹点采集> 120Hz(Pointer Events 合并)
百页 PDF 内存< 200MB(分页卸载)
导出 10MB PDF< 3s