Signature Pad 源码解析基于贝塞尔曲线的数字签名平滑绘制实战【免费下载链接】signature_padHTML5 canvas based smooth signature drawing项目地址: https://gitcode.com/gh_mirrors/si/signature_padSignature Pad 是一款基于 HTML5 Canvas 的 JavaScript 签名绘制库专为 Web 应用中的数字签名场景设计。通过创新的贝塞尔曲线插值算法它实现了接近真实笔迹的流畅签名体验特别适用于电子合同、表单签署、文档验证等企业级应用场景。该库采用 TypeScript 编写无外部依赖gzip 压缩后仅 8KB是现代 Web 开发中处理数字签名的首选解决方案。核心技术原理贝塞尔曲线插值算法Signature Pad 的核心技术源自 Square 公司的平滑签名算法研究通过三阶贝塞尔曲线实现笔迹的流畅过渡。算法根据用户绘制时的速度和压力动态调整线条宽度模拟真实笔迹的粗细变化。贝塞尔曲线控制点计算在 src/bezier.ts 中控制点的计算基于三个连续采样点private static calculateControlPoints( s1: BasicPoint, s2: BasicPoint, s3: BasicPoint, ): { c1: BasicPoint; c2: BasicPoint; } { const dx1 s1.x - s2.x; const dy1 s1.y - s2.y; const dx2 s2.x - s3.x; const dy2 s2.y - s3.y; const m1 { x: (s1.x s2.x) / 2.0, y: (s1.y s2.y) / 2.0 }; const m2 { x: (s2.x s3.x) / 2.0, y: (s2.y s3.y) / 2.0 }; const l1 Math.sqrt(dx1 * dx1 dy1 * dy1); const l2 Math.sqrt(dx2 * dx2 dy2 * dy2); const dxm m1.x - m2.x; const dym m1.y - m2.y; const k l1 l2 0 ? 0 : l2 / (l1 l2); const cm { x: m2.x dxm * k, y: m2.y dym * k }; const tx s2.x - cm.x; const ty s2.y - cm.y; return { c1: new Point(m1.x tx, m1.y ty), c2: new Point(m2.x tx, m2.y ty), }; }速度感知的线条宽度计算在 src/signature_pad.ts 中线条宽度根据绘制速度动态调整private _calculateCurveWidths( startPoint: Point, endPoint: Point, ): { start: number; end: number } { const velocity this.options.velocityFilterWeight * endPoint.velocity (1 - this.options.velocityFilterWeight) * this._lastVelocity; const newWidth this._strokeWidth(velocity); const widths { start: this._lastWidth, end: newWidth, }; this._lastVelocity velocity; this._lastWidth newWidth; return widths; } private _strokeWidth(velocity: number): number { return Math.max( this.options.maxWidth / (velocity 1), this.options.minWidth, ); }架构设计与性能优化策略事件驱动的架构设计Signature Pad 采用事件驱动架构继承自 EventTarget支持完整的生命周期事件监听// 继承自 SignatureEventTarget export default class SignaturePad extends SignatureEventTarget { // 支持的事件类型 public static readonly EVENT_BEGIN_STROKE beginStroke; public static readonly EVENT_END_STROKE endStroke; public static readonly EVENT_BEFORE_UPDATE_STROKE beforeUpdateStroke; public static readonly EVENT_AFTER_UPDATE_STROKE afterUpdateStroke; }节流与性能优化在 src/throttle.ts 中实现了高效的节流机制确保在高频输入下的性能表现export function throttleT extends (...args: any[]) any( fn: T, wait 16, ): (...args: ParametersT) void { let timeout: number | undefined; let lastTime 0; return function (...args: ParametersT) { const now Date.now(); const remaining wait - (now - lastTime); if (remaining 0 || remaining wait) { if (timeout) { clearTimeout(timeout); timeout undefined; } lastTime now; fn(...args); } else if (!timeout) { timeout window.setTimeout(() { lastTime Date.now(); timeout undefined; fn(...args); }, remaining); } }; }实战集成指南与最佳实践现代化 TypeScript 集成import SignaturePad from signature_pad; // 类型安全的配置选项 interface SignatureConfig { dotSize?: number; minWidth?: number; maxWidth?: number; penColor?: string; backgroundColor?: string; throttle?: number; minDistance?: number; velocityFilterWeight?: number; } const config: SignatureConfig { minWidth: 0.5, maxWidth: 2.5, penColor: #1a73e8, backgroundColor: #f8f9fa, throttle: 16, minDistance: 5, }; const canvas document.getElementById(signatureCanvas) as HTMLCanvasElement; const signaturePad new SignaturePad(canvas, config);响应式 Canvas 处理高 DPI 屏幕适配是签名库的关键挑战以下是完整的响应式解决方案class ResponsiveSignaturePad { private signaturePad: SignaturePad; private canvas: HTMLCanvasElement; constructor(canvasId: string, options?: SignatureConfig) { this.canvas document.getElementById(canvasId) as HTMLCanvasElement; this.signaturePad new SignaturePad(this.canvas, options); this.setupResponsiveBehavior(); } private setupResponsiveBehavior(): void { // 初始调整 this.resizeCanvas(); // 响应窗口大小变化 window.addEventListener(resize, this.debounce(() { this.resizeCanvas(); }, 250)); // 处理设备方向变化 window.addEventListener(orientationchange, () { setTimeout(() this.resizeCanvas(), 100); }); } private resizeCanvas(): void { const ratio Math.max(window.devicePixelRatio || 1, 1); const { offsetWidth, offsetHeight } this.canvas; // 保存当前签名数据 const data this.signaturePad.toData(); // 调整 Canvas 尺寸 this.canvas.width offsetWidth * ratio; this.canvas.height offsetHeight * ratio; this.canvas.getContext(2d)?.scale(ratio, ratio); // 恢复签名 this.signaturePad.fromData(data, { clear: false }); } private debounce(func: Function, wait: number): () void { let timeout: number; return () { clearTimeout(timeout); timeout window.setTimeout(() func(), wait); }; } }数据持久化与服务器集成// 签名数据序列化与反序列化 class SignatureManager { static saveSignature(pad: SignaturePad, key: string): void { const signatureData { timestamp: Date.now(), data: pad.toData(), svg: pad.toSVG(), png: pad.toDataURL(image/png), }; localStorage.setItem(key, JSON.stringify(signatureData)); } static loadSignature(pad: SignaturePad, key: string): boolean { const saved localStorage.getItem(key); if (!saved) return false; const { data } JSON.parse(saved); pad.fromData(data); return true; } static async uploadToServer( pad: SignaturePad, endpoint: string ): PromiseResponse { const formData new FormData(); // PNG 格式上传 const pngData pad.toDataURL(image/png); const pngBlob await this.dataURLtoBlob(pngData); formData.append(signature_png, pngBlob, signature.png); // SVG 矢量格式上传 const svgData pad.toSVG(); const svgBlob new Blob([svgData], { type: image/svgxml }); formData.append(signature_svg, svgBlob, signature.svg); // 原始点数据上传用于重绘和验证 const pointData pad.toData(); formData.append(signature_data, JSON.stringify(pointData)); return fetch(endpoint, { method: POST, body: formData, }); } private static dataURLtoBlob(dataURL: string): PromiseBlob { return fetch(dataURL).then(res res.blob()); } }高级功能扩展开发指南自定义笔刷系统// 扩展 SignaturePad 支持自定义笔刷 class CustomBrushSignaturePad extends SignaturePad { private brushPatterns: Mapstring, CanvasPattern new Map(); constructor(canvas: HTMLCanvasElement, options?: Options) { super(canvas, options); this.initBrushPatterns(); } private initBrushPatterns(): void { // 创建虚线笔刷 this.createDashedPattern(); // 创建纹理笔刷 this.createTexturePattern(); } private createDashedPattern(): void { const patternCanvas document.createElement(canvas); patternCanvas.width 10; patternCanvas.height 10; const ctx patternCanvas.getContext(2d)!; ctx.strokeStyle this.penColor; ctx.lineWidth 2; ctx.setLineDash([5, 5]); ctx.beginPath(); ctx.moveTo(0, 5); ctx.lineTo(10, 5); ctx.stroke(); const pattern ctx.createPattern(patternCanvas, repeat)!; this.brushPatterns.set(dashed, pattern); } // 重写绘制方法使用自定义笔刷 protected _drawCurve( startPoint: Point, endPoint: Point, startWidth: number, endWidth: number ): void { const ctx this._ctx; const brushType this.getCurrentBrushType(); if (brushType dashed this.brushPatterns.has(dashed)) { ctx.strokeStyle this.brushPatterns.get(dashed)!; } else { ctx.strokeStyle this.penColor; } // 调用父类绘制逻辑 super._drawCurve(startPoint, endPoint, startWidth, endWidth); } }签名验证与质量检测// 签名质量验证系统 class SignatureValidator { static validateSignature(pad: SignaturePad): ValidationResult { const data pad.toData(); const stats this.calculateSignatureStats(data); return { isValid: this.checkValidity(stats), score: this.calculateQualityScore(stats), issues: this.detectIssues(stats), stats, }; } private static calculateSignatureStats(data: PointGroup[]): SignatureStats { let totalPoints 0; let totalLength 0; let minX Infinity; let maxX -Infinity; let minY Infinity; let maxY -Infinity; data.forEach(group { totalPoints group.points.length; // 计算笔画长度 for (let i 1; i group.points.length; i) { const p1 group.points[i - 1]; const p2 group.points[i]; totalLength Math.sqrt( Math.pow(p2.x - p1.x, 2) Math.pow(p2.y - p1.y, 2) ); } // 计算边界 group.points.forEach(point { minX Math.min(minX, point.x); maxX Math.max(maxX, point.x); minY Math.min(minY, point.y); maxY Math.max(maxY, point.y); }); }); return { totalPoints, totalLength, boundingBox: { width: maxX - minX, height: maxY - minY, area: (maxX - minX) * (maxY - minY), }, pointDensity: totalPoints / (totalLength || 1), }; } private static checkValidity(stats: SignatureStats): boolean { // 有效签名至少需要一定数量的点和面积 return ( stats.totalPoints 20 stats.boundingBox.area 1000 stats.totalLength 50 ); } }测试驱动开发与质量保证Signature Pad 拥有完善的测试套件在 tests/ 目录中包含了全面的单元测试和集成测试// 测试高 DPI 屏幕适配 describe(High DPI support, () { it(correctly scales canvas for devicePixelRatio 1, () { const originalDPR window.devicePixelRatio; window.devicePixelRatio 2; const canvas document.createElement(canvas); canvas.style.width 300px; canvas.style.height 150px; const pad new SignaturePad(canvas); pad.resizeCanvas(); expect(canvas.width).toBe(600); // 300 * 2 expect(canvas.height).toBe(300); // 150 * 2 window.devicePixelRatio originalDPR; }); }); // 测试贝塞尔曲线算法 describe(Bezier curve interpolation, () { it(creates smooth curves from point data, () { const points [ new Point(0, 0), new Point(50, 100), new Point(100, 50), new Point(150, 150), ]; const bezier Bezier.fromPoints(points, { start: 1, end: 2 }); expect(bezier).toBeInstanceOf(Bezier); expect(bezier.length()).toBeGreaterThan(0); }); });性能优化与内存管理Canvas 状态管理优化class OptimizedSignaturePad extends SignaturePad { private drawingCache: ImageData[] []; private cacheLimit 10; constructor(canvas: HTMLCanvasElement, options?: Options) { super(canvas, options); this.setupMemoryManagement(); } private setupMemoryManagement(): void { // 监听内存压力事件 if (memory in performance) { performance.addEventListener(memorypressure, () { this.cleanupMemory(); }); } // 定期清理缓存 setInterval(() this.cleanupMemory(), 30000); } protected _drawCurve( startPoint: Point, endPoint: Point, startWidth: number, endWidth: number ): void { // 在绘制前保存 Canvas 状态 this._ctx.save(); try { super._drawCurve(startPoint, endPoint, startWidth, endWidth); // 缓存当前绘制状态 if (this.drawingCache.length this.cacheLimit) { const imageData this._ctx.getImageData( 0, 0, this.canvas.width, this.canvas.height ); this.drawingCache.push(imageData); } } finally { this._ctx.restore(); } } private cleanupMemory(): void { // 清理过期的缓存 if (this.drawingCache.length this.cacheLimit / 2) { this.drawingCache this.drawingCache.slice(-Math.floor(this.cacheLimit / 2)); } } }触摸事件优化// 移动端触摸事件优化 class TouchOptimizedSignaturePad extends SignaturePad { private touchPoints: Mapnumber, Point new Map(); private isPinching false; private pinchStartDistance 0; constructor(canvas: HTMLCanvasElement, options?: Options) { super(canvas, options); this.setupTouchOptimizations(); } private setupTouchOptimizations(): void { // 禁用默认触摸行为 this.canvas.style.touchAction none; // 添加多点触控支持 this.canvas.addEventListener(touchstart, this.handleTouchStart.bind(this), { passive: false, }); this.canvas.addEventListener(touchmove, this.handleTouchMove.bind(this), { passive: false, }); this.canvas.addEventListener(touchend, this.handleTouchEnd.bind(this)); } private handleTouchStart(event: TouchEvent): void { event.preventDefault(); if (event.touches.length 2) { // 双指操作缩放或旋转 this.isPinching true; this.pinchStartDistance this.getTouchDistance(event.touches); return; } // 单指操作正常绘制 for (let i 0; i event.touches.length; i) { const touch event.touches[i]; const point this._getPoint(touch); this.touchPoints.set(touch.identifier, point); } } private getTouchDistance(touches: TouchList): number { const dx touches[0].clientX - touches[1].clientX; const dy touches[0].clientY - touches[1].clientY; return Math.sqrt(dx * dx dy * dy); } }总结与展望Signature Pad 作为一款成熟的数字签名解决方案通过精妙的贝塞尔曲线算法和优化的性能设计为 Web 开发者提供了企业级的签名功能支持。其源码结构清晰模块化设计良好易于扩展和定制。未来可能的改进方向包括WebGL 加速渲染利用 WebGL 实现更高效的曲线渲染机器学习验证集成机器学习模型进行签名真伪验证跨平台支持扩展对 React Native、Flutter 等框架的支持实时协作支持多人协同签名和实时预览通过深入理解 Signature Pad 的源码实现开发者可以更好地定制和优化签名体验满足各种复杂的业务需求。该项目的测试用例在 tests/ 目录中提供了丰富的使用示例是学习 Canvas 绘图和交互设计的优秀参考资源。【免费下载链接】signature_padHTML5 canvas based smooth signature drawing项目地址: https://gitcode.com/gh_mirrors/si/signature_pad创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考