纯前端实现 Canvas 手写签名:指针压力、贝塞尔平滑与离线导出

举报
yd_213843956 发表于 2026/10/28 07:05:53 2026/10/28
【摘要】 手写签名看着简单,真做起来要处理笔迹抖动、压感粗细和导出清晰度。这篇讲指针事件采样、二次贝塞尔平滑和离线导出的三个关键点。

电子签名是一个很典型的「看起来简单、细节不少」的需求。用户拿鼠标或手指划几笔,页面要画出顺滑的线条,还要能导出成图片。这篇文章把实现拆成三块:怎么采样、怎么平滑、怎么导出。

一、用指针事件统一鼠标和触摸

不要分别监听 mousemove 和 touchmove,直接用 Pointer Events,一套逻辑覆盖鼠标、触控和触控笔:

canvas.addEventListener('pointerdown', start);
canvas.addEventListener('pointermove', move);
canvas.addEventListener('pointerup', end);
canvas.addEventListener('pointercancel', end);

开始绘制时调用 setPointerCapture,把后续的移动事件都绑到当前元素上,这样指针滑出画布再回来不会断线。触屏上还要加 touch-action: none,避免浏览器把滑动当成页面滚动。

二、采样:别把每个点都当成折线拐点

高刷新率设备一秒能上报上百个点,如果每个点都 lineTo,线条会显得生硬而且文件里的点数爆炸。常见做法是三点一组,用二次贝塞尔把中点当锚点:

ctx.moveTo(p0.x, p0.y);
for (let i = 1; i < pts.length - 1; i++) {
  const mx = (pts[i].x + pts[i + 1].x) / 2;
  const my = (pts[i].y + pts[i + 1].y) / 2;
  ctx.quadraticCurveTo(pts[i].x, pts[i].y, mx, my);
}

先用原始点当控制点、中点当终点,线条会自然很多。还可以顺手做一次距离过滤,两点距离低于 1~2 像素就跳过,去掉抖动噪声。

三、压感控制粗细

触控笔会给出 PointerEvent.pressure,可以在 0 到 1 之间。把线宽映射成一个范围,就能写出有轻重变化的字:

const w = maxWidth - (maxWidth - minWidth) * pressure;
ctx.lineWidth = w;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';

鼠标通常只有 0.5 的固定压力,这时直接用一个默认宽度即可。真正的难点是笔锋变化靠逐段设置 lineWidth 会显得一节一节,想更顺可以每段单独 stroke,或者用变宽路径填充。

四、导出:先换算设备像素比

导出的图片如果模糊,多半是没处理 devicePixelRatio。画布的内部尺寸要按 DPR 放大,CSS 尺寸保持逻辑像素:

const dpr = window.devicePixelRatio || 1;
canvas.width = cssW * dpr;
canvas.height = cssH * dpr;
ctx.scale(dpr, dpr);

导出时用一个临时画布,填白色背景再画上笔迹,避免 PNG 透明背景导致部分场景看不见:

const url = canvas.toDataURL('image/png');

需要提交给后端时,用 toBlob 拿二进制比 toDataURL 更省内存。

五、重绘与清空

撤销一笔最简单的做法是保存每一笔的点数组,重绘时把前面几笔全部再画一遍。笔数不多时够用,笔数很多时可以只存路径点、按需重放。清空就是把数组置空并 clearRect。

导出前记得判断是否为空,空签名不要生成图片,直接提示用户先书写。

六、小结

手写签名的核心是采样和平滑:Pointer Events 统一输入,二次贝塞尔让线条顺滑,压感决定粗细,DPR 决定导出清晰度。把这几件事拆开,代码量不大,但手感差别很明显。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。