电子手写签批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 |