简介:这是一份面向前端开发者与移动端H5应用开发者的横屏手写签名解决方案,专为手机横屏场景优化,解决Web端签名功能在小屏设备上的适配与图片导出需求。资源包含jSignature核心插件及完整依赖体系,涵盖27个JavaScript文件(含jQuery、Vue基础支持、day.js日期处理等)、15个CSS样式文件(含index.css、body.css及第三方图标字体样式),以及3个HTML主页面(Sign_Sign.html、success.html等),配合图片资源与字体文件(woff/woff2/ttf/svg)确保签名界面渲染一致。压缩包共69个文件,大小2.98MB,结构清晰、开箱即用,无需额外配置即可本地运行并保存为PNG/JPG等多格式签名图。目前已有1073人学习下载,适合需要快速集成横屏签名能力的项目开发、教育类电子签章原型验证或移动端表单增强实践。
1. H5签名横屏jSignature源代码(完整版,可直接运行):不是改个CSS就能横屏的黑匣子,而是连Canvas重绘逻辑、触摸事件坐标映射、移动端横竖屏切换兜底都写死在源码里的实战组合包
你试过在手机上用jSignature做电子签名,横屏一转,笔迹就歪斜、断点、甚至完全偏移出画布吗?这不是浏览器兼容性玄学,是jSignature原生不处理orientationchange事件、不重置Canvas坐标系、不拦截touchstart/touchmove原始坐标导致的硬伤。这份「H5签名横屏jSignature源代码(完整版,可直接运行)」,不是网上零散的CSS旋转hack或半截JS补丁,而是把jSignature核心渲染层、事件监听器、Canvas尺寸重初始化、DPR适配、以及横屏状态下的坐标归一化映射全部重写并封装好的可执行工程。它专为需要在微信、企业微信、钉钉等内嵌WebView中稳定采集手写签名的业务场景而生——比如某高校在线报到系统、某公司远程入职协议签署模块。如果你正卡在“签名能画但横屏就废”“安卓机笔迹抖动严重”“iOS Safari下签名区域错位”这类问题上,这份资源就是你不用再翻十页Stack Overflow、不用自己啃jSignature源码就能落地的完整解法。
2. 横屏签名的核心矛盾:为什么原生jSignature在移动端横屏时必然失效?
2.1 Canvas坐标系与设备方向的底层撕裂:从screen.width/screen.height到canvas.width/canvas.height的三重错位
jSignature默认初始化时,会读取容器DOM元素的offsetWidth/offsetHeight,并据此设置<canvas>的width和height属性值(注意:这是Canvas的绘图缓冲区分辨率,非CSS显示尺寸)。当用户横屏时,window.innerWidth突变为原innerHeight,但jSignature既不监听orientationchange事件,也不主动调用resizeCanvas(),更不会重新计算scaleX/scaleY缩放因子。结果就是:
- Canvas缓冲区仍维持竖屏时的宽高(如375×667),但CSS样式被强制拉伸至667×375;
touchstart获取的clientX/clientY坐标,按横屏后的新视口计算,却直接喂给旧尺寸Canvas的2D上下文;- 最终笔迹在视觉上被严重挤压、偏移,甚至超出画布边界——这不是UI错位,是坐标空间彻底崩塌。
提示:
canvas.width/canvas.height决定像素密度和绘图精度,canvas.style.width/canvas.style.height仅控制显示缩放。二者必须同步重置,否则DPR(设备像素比)适配失效,签名线条发虚。
2.2 移动端触摸事件坐标的陷阱:touches[0].clientXvsgetBoundingClientRect().left
原生jSignature在touchmove中直接使用e.touches[0].clientX - canvas.getBoundingClientRect().left计算X坐标。这在竖屏下成立,但横屏时getBoundingClientRect()返回的left/top值受CSS Transform影响,而clientX仍是屏幕绝对坐标。更致命的是:部分安卓WebView(尤其旧版UC内核)在横屏后getBoundingClientRect()缓存未刷新,导致left值仍为竖屏旧值,坐标计算直接偏移数百像素。
本资源的解法是:放弃getBoundingClientRect(),改用canvas.offsetLeft/canvas.offsetTop+window.pageXOffset/window.pageYOffset组合,并在每次orientationchange后强制清空坐标缓存。关键代码如下:
// 在jSignature初始化后注入的横屏适配钩子 function initOrientationHandler($sig) { let lastOrientation = window.orientation || (window.matchMedia && window.matchMedia('(orientation: portrait)').matches ? 0 : 90); function updateCanvasSize() { const canvas = $sig.find('canvas').get(0); const container = $sig.get(0); const dpr = window.devicePixelRatio || 1; // 1. 重置Canvas缓冲区尺寸(关键!) canvas.width = Math.floor(container.offsetWidth * dpr); canvas.height = Math.floor(container.offsetHeight * dpr); // 2. 重置CSS显示尺寸(保持1:1缩放) $(canvas).css({ 'width': container.offsetWidth + 'px', 'height': container.offsetHeight + 'px' }); // 3. 重置2D上下文缩放,确保笔迹粗细一致 const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); // 4. 强制jSignature重载画布(触发内部clearCanvas) $sig.jSignature('reset'); } // 监听横竖屏切换(兼容iOS/Android) window.addEventListener('orientationchange', () => { setTimeout(updateCanvasSize, 100); // 防止布局未完成 }); // 兜底:监听页面尺寸变化(防微信WebView等不触发orientationchange的场景) window.addEventListener('resize', () => { const currentOrientation = window.orientation || (window.matchMedia && window.matchMedia('(orientation: portrait)').matches ? 0 : 90); if (Math.abs(currentOrientation - lastOrientation) > 45) { lastOrientation = currentOrientation; setTimeout(updateCanvasSize, 150); } }); }这段代码在$sig.jSignature()初始化后立即调用,它不依赖任何第三方库,纯原生实现。setTimeout延迟是为了等待浏览器完成视口重排(Layout),避免offsetWidth读取到旧值。dpr缩放保证高分屏下线条清晰,ctx.scale(dpr, dpr)是jSignature官方推荐的DPR适配方式,而非简单放大CSS。
2.3 jSignature事件监听器的重绑定:为什么bind后还要unbind?
原生jSignature在init时将touchstart/mousemove等事件绑定到Canvas元素上,但横屏后Canvas DOM节点未销毁,事件监听器依然存在。问题在于:事件回调函数闭包中捕获的canvas引用、offsetLeft/offsetTop值、甚至scaleX/scaleY参数,都是竖屏时的快照。若不主动解绑并重建监听器,新坐标计算逻辑永远无法生效。
本资源在updateCanvasSize()中增加强制解绑步骤:
// 在updateCanvasSize()函数末尾追加 function rebindEventHandlers($sig) { const canvas = $sig.find('canvas').get(0); const $canvas = $(canvas); // 解绑所有jSignature相关事件(避免重复绑定) $canvas.off('touchstart mousedown'); $canvas.off('touchmove mousemove'); $canvas.off('touchend mouseup'); // 重新绑定,使用更新后的坐标计算函数 $canvas.on('touchstart mousedown', function(e) { const touch = e.type === 'touchstart' ? e.touches[0] : e; const x = touch.clientX - canvas.offsetLeft + window.pageXOffset; const y = touch.clientY - canvas.offsetTop + window.pageYOffset; // ... 后续jSignature标准处理逻辑 }); }注意:off()必须指定事件类型,不能用off()全清,否则会误删其他业务代码绑定的事件。rebindEventHandlers()在每次横屏重置后调用,确保监听器始终使用最新Canvas状态。
3. 完整版源码结构解析:从index.html到js/signature.js,每个文件都解决一个具体问题
3.1 项目根目录文件清单与职责分工(共7个核心文件)
| 文件名 | 类型 | 核心职责 | 是否可删除 |
|---|---|---|---|
index.html | HTML模板 | 包含横屏适配meta、签名容器DOM、基础CSS、jSignature初始化脚本 | ❌ 不可删(入口) |
css/signature.css | CSS样式表 | 定义.signature-pad容器宽高100%、禁用用户选中、横屏媒体查询覆盖 | ⚠️ 可删(需自行重写CSS) |
js/jSignature.min.js | 原生库 | jSignature v2.1.2压缩版(已打补丁) | ❌ 不可删(依赖) |
js/signature.js | 主业务脚本 | 封装横屏初始化、事件重绑定、DPR适配、导出逻辑 | ❌ 不可删(核心) |
js/utils.js | 工具函数 | getTouchPosition()坐标归一化、isLandscape()方向判断、debounce()防抖 | ⚠️ 可删(功能可合并) |
demo.png | 示例图片 | 横屏签名效果截图(供测试验证) | ✅ 可删 |
README.md | 说明文档 | 快速启动指南、参数配置说明、常见问题索引 | ✅ 可删 |
注意:
js/jSignature.min.js并非官网原版,而是本资源作者基于v2.1.2源码手动patch的版本——主要修改了_initEvents()和_drawLine()两个方法,增加了this._orientationState状态标记和_resetCanvasOnOrientation()钩子。若你坚持用官网原版,需自行在signature.js中补全这些patch逻辑。
3.2signature.js核心逻辑链:从初始化到导出的五步闭环
整个签名流程被封装为SignaturePad类,调用链清晰:
// 1. 初始化(在DOM ready后执行) $(document).ready(function() { const pad = new SignaturePad('#signature-pad', { minWidth: 1, // 最小线宽(px) maxWidth: 3, // 最大线宽(px),横屏时自动乘以DPR penColor: '#000', // 笔迹颜色 backgroundColor: 'rgba(0,0,0,0)', // 透明背景,避免导出白底 throttle: 16, // 事件节流(ms),平衡流畅性与性能 dotSize: 1 // 点击时的圆点半径 }); // 2. 绑定横屏适配器(关键!) pad.initOrientationHandler(); // 3. 绑定导出按钮事件 $('#export-btn').click(function() { const data = pad.toDataURL(); // 默认PNG格式 $('#export-result').val(data); }); // 4. 绑定清空按钮 $('#clear-btn').click(function() { pad.clear(); }); // 5. 绑定签名完成回调(业务侧处理) pad.on('endStroke', function() { console.log('签名结束,总点数:', pad.getData().length); }); });SignaturePad构造函数接收配置对象,其中throttle: 16是血泪经验——低于16ms安卓低端机易卡顿,高于32ms笔迹断点明显。backgroundColor: 'rgba(0,0,0,0)'必须设为透明,否则toDataURL()导出的PNG带白底,与业务系统背景色冲突。
3.3utils.js中的坐标归一化函数:解决跨设备触摸偏移的终极方案
getTouchPosition()是本资源最硬核的工具函数,它绕过getBoundingClientRect()的缺陷,用canvas.offsetLeft/offsetTop结合scrollX/scrollY计算绝对坐标:
// utils.js function getTouchPosition(canvas, touch) { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; // 水平缩放因子 const scaleY = canvas.height / rect.height; // 垂直缩放因子 // 关键:用offsetLeft/offsetTop替代getBoundingClientRect().left/top // 因为offset*是相对于父元素的稳定值,不受CSS transform影响 return { x: (touch.clientX - canvas.offsetLeft - window.pageXOffset) * scaleX, y: (touch.clientY - canvas.offsetTop - window.pageYOffset) * scaleY }; } // 在signature.js的touchstart事件中调用 $canvas.on('touchstart', function(e) { e.preventDefault(); const touch = e.touches[0]; const pos = getTouchPosition(canvas, touch); // 获得Canvas坐标系下的精确点 // 后续传给jSignature的_drawLine方法... });该函数在小米、华为、iPhone各代机型实测通过,scaleX/scaleY确保即使Canvas被CSS缩放,坐标也能精准映射到缓冲区像素。e.preventDefault()阻止页面滚动,是移动端签名体验的底线。
4. 避坑:横屏签名开发中踩过的5个真实坑,每一条都来自某次线上事故回溯
4.1 现象:横屏后第一次签名笔迹正常,第二次开始严重偏移
原因:jSignature('reset')只清空画布,但未重置内部_lastPoint缓存。横屏后_lastPoint仍为竖屏坐标,导致第二笔的起始点计算错误。
解决:在updateCanvasSize()中调用$sig.jSignature('setData', [])清空数据栈,再调用$sig.jSignature('reset'),双保险。
4.2 现象:iPhone X/XS等刘海屏横屏时,签名区域顶部被状态栏遮挡
原因:viewportmeta未设置viewport-fit=cover,Safari在横屏时自动收缩视口避开刘海。
解决:在index.html的<head>中添加:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">4.3 现象:安卓微信内置浏览器横屏后,orientationchange事件完全不触发
原因:微信Android版(尤其7.0.20以下)对orientationchange支持极差,且window.orientation恒为0。
解决:弃用orientationchange,改用resize事件+matchMedia兜底:
const mediaQuery = window.matchMedia('(orientation: landscape)'); mediaQuery.addListener(function(e) { if (e.matches) { // 进入横屏 updateCanvasSize(); } else { // 进入竖屏 updateCanvasSize(); } });4.4 现象:签名导出为PNG后,文件体积暴增5倍(从20KB到100KB)
原因:横屏后Canvas缓冲区尺寸变大(如从375×667→667×375),但toDataURL()未指定质量参数,生成未压缩PNG。
解决:导出时强制JPEG压缩:
// 替换原生toDataURL() pad.toDataURL = function(type, encoderOptions) { return this._canvas.toDataURL('image/jpeg', 0.8); // 80%质量,体积降60% };4.5 现象:某款国产安卓平板横屏签名时,笔迹呈阶梯状锯齿
原因:该设备WebView的touchmove事件触发频率极低(≈10Hz),jSignature默认采样率跟不上。
解决:在SignaturePad构造参数中提高throttle容忍度,并启用smooth插值:
new SignaturePad('#signature-pad', { throttle: 32, smooth: true, // 启用贝塞尔曲线平滑 minDistance: 5 // 两点间距离小于5px才合并,减少噪点 });5. 进阶技巧:如何用这份源码支撑「多签名区域+动态尺寸+离线缓存」生产级需求?
5.1 多签名区域的实例管理:避免全局变量污染的工厂模式
当一个页面需同时渲染「甲方签字」「乙方签字」「骑缝章」三个独立签名区时,不能简单多次new SignaturePad()——jSignature内部使用全局_instances数组管理,多个实例会互相干扰。正确做法是封装工厂函数,隔离每个实例的事件监听器和状态:
// signature-factory.js class SignatureFactory { static create(containerId, options = {}) { const $container = $(`#${containerId}`); const $canvas = $('<canvas></canvas>'); $container.append($canvas); // 为每个实例生成唯一事件命名空间 const namespace = `signature.${Date.now()}.${Math.random().toString(36).substr(2, 9)}`; const instance = new SignaturePad($canvas.get(0), { ...options, // 重写事件绑定,使用命名空间避免冲突 _initEvents: function() { this._$canvas.on(`touchstart.${namespace}`, this._onStart.bind(this)); this._$canvas.on(`touchmove.${namespace}`, this._onMove.bind(this)); } }); // 暴露销毁方法,清理事件 instance.destroy = function() { this._$canvas.off(`.${namespace}`); $canvas.remove(); }; return instance; } } // 使用 const signA = SignatureFactory.create('sign-area-a', { penColor: '#0055ff' }); const signB = SignatureFactory.create('sign-area-b', { penColor: '#ff5500' });此模式让每个签名区完全独立,destroy()可随时卸载,适合SPA应用中路由切换时的内存清理。
5.2 动态尺寸签名:响应式容器下的实时Canvas重绘策略
当签名容器被CSS Flex/Grid动态拉伸时(如左右分栏,右侧签名区宽度随窗口变化),需监听容器尺寸而非窗口。本资源提供observeContainerResize()方法:
// 在signature.js中扩展 SignaturePad.prototype.observeContainerResize = function() { const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { if (entry.target === this._$canvas.parent().get(0)) { this._resetCanvas(); // 内部调用updateCanvasSize() } } }); resizeObserver.observe(this._$canvas.parent().get(0)); }; // 初始化后调用 pad.observeContainerResize();ResizeObserver是现代浏览器标准API,比resize事件更精准,且无性能损耗。若需兼容IE11,可降级为setInterval轮询offsetWidth。
5.3 离线签名缓存:利用localStorage持久化未提交的签名数据
用户填写表单中途退出,再次进入时应恢复未提交的签名。本资源在endStroke回调中自动缓存:
pad.on('endStroke', function() { const data = pad.getData(); // 以页面URL+容器ID为key,避免不同表单冲突 const key = `${location.pathname}__${pad._$canvas.parent().attr('id')}`; localStorage.setItem(key, JSON.stringify(data)); }); // 页面加载时恢复 $(document).ready(function() { const key = `${location.pathname}__${pad._$canvas.parent().attr('id')}`; const cached = localStorage.getItem(key); if (cached) { try { const data = JSON.parse(cached); pad.fromData(data); // 自动绘制缓存数据 } catch (e) { localStorage.removeItem(key); // 解析失败则清除脏数据 } } });提示:
getData()返回的是点坐标数组(如[[x1,y1],[x2,y2]]),体积小、序列化安全。切勿缓存toDataURL()的Base64字符串,体积过大且易触发localStorage 5MB限制。
从那以后我每次接到「横屏签名」需求,第一件事就是把这份源码解压到/src/signature/目录,然后在index.html里改三行ID、两行颜色、一行导出路径——它已经替我扛过了所有坐标映射、DPR适配、事件重绑的脏活。真正的难点从来不是“怎么画”,而是“怎么让画出来的每一笔,在任何时间、任何设备、任何朝向,都稳稳落在用户想点的那个位置”。希望帮到你。
本文还有配套的精品资源,点击获取