Signature签名组件在移动端显示错位怎么办?

书生シ东宁 阅读 31

我用Canvas做的签名组件,在PC上正常,但一到手机上就偏移严重,手指点的位置和画出来的线对不上,特别影响体验。

试过加了 touch-action: none,也调整了canvas的宽高比,但还是不行。是不是transform或者缩放的问题?下面是我目前的样式:

.signature-canvas {
  width: 100%;
  height: 200px;
  border: 1px solid #ccc;
  touch-action: none;
  /* 尝试过加这个也没用 */
  /* transform: scale(1); */
}
我来解答 赞 6 收藏
二维码
手机扫码查看
1 条解答
设计师丽敏
这个问题太经典了,几乎每个做签名板的新手都会踩这个坑。简单来说,就是 Canvas 的 CSS 显示尺寸(屏幕上看起来多大)和它的内部绘图分辨率(实际有多少个像素点)不一致导致的。

你现在的 CSS 把 Canvas 拉伸了,但 JS 里获取坐标的时候,如果不做比例换算,画笔位置肯定偏。加上移动端还有各种视口缩放、设备像素比(DPR)的问题,这就乱套了。

别去折腾 transform 了,解决思路其实就一句话:算出 CSS 宽高和 Canvas 属性宽高的比例,然后把触摸坐标按这个比例缩放回去。

这里分三步来解决:

第一步,正确初始化 Canvas 的尺寸。
很多时候我们直接在 HTML 标签写 width="100%" 是没用的,必须在 JS 里动态设置它的属性宽高,让它等于 CSS 渲染出来的实际像素宽高。如果想让线条在高清屏上不模糊,还得考虑设备像素比。

第二步,获取准确的坐标偏移量。
千万别用 offsetTop/offsetLeft,这玩意儿在有父级定位或者滚动条的时候会算错。一定要用 getBoundingClientRect(),这个方法能拿到 Canvas 相对于浏览器视口的精确位置。

第三步,核心的坐标转换公式。
当你手指触摸屏幕时,拿到的是屏幕坐标,你需要减去 Canvas 的偏移量,再乘以缩放比例。公式是:Canvas坐标 = (触摸坐标 - Canvas偏移) * (Canvas内部宽度 / CSS显示宽度)。

下面给你一段可以直接用的代码,你把原来获取坐标的逻辑替换成这个试试:

// 获取 Canvas 元素
var canvas = document.querySelector('.signature-canvas');
var ctx = canvas.getContext('2d');

// 初始化 Canvas 尺寸的函数
function initCanvas() {
// 获取 Canvas 在屏幕上实际显示的尺寸
var rect = canvas.getBoundingClientRect();

// 获取设备像素比,处理高清屏模糊问题,普通屏就是 1
var dpr = window.devicePixelRatio || 1;

// 设置 Canvas 的内部绘图分辨率 = CSS尺寸 * 像素比
// 这里注意:要设置 width/height 属性,而不是 style.width/height
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;

// 用 CSS 强制把 Canvas 显示大小固定回去
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';

// 缩放绘图上下文,保证笔触粗细视觉一致
ctx.scale(dpr, dpr);

// 设置一下笔触样式,这个看你喜好
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.strokeStyle = '#000';
}

// 页面加载或窗口大小改变时重新初始化
window.addEventListener('load', initCanvas);
// 如果你的布局会随窗口变动,resize 事件最好也加上
// window.addEventListener('resize', initCanvas);

// 获取坐标的核心函数
function getPos(e) {
var rect = canvas.getBoundingClientRect();

// 这里需要注意:触摸事件和鼠标事件取值方式不一样
var clientX, clientY;
if (e.touches && e.touches.length > 0) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}

// 计算缩放比例:Canvas内部宽度 / CSS显示宽度
// 因为我们上面用 dpr 放过 canvas.width,所以这里比例其实就是 dpr
// 但为了通用性(万一你没按我上面写),还是算一下比较稳妥
var scaleX = canvas.width / rect.width;
var scaleY = canvas.height / rect.height;

return {
x: (clientX - rect.left) * scaleX,
y: (clientY - rect.top) * scaleY
};
}

// 绘图事件监听
var isDrawing = false;

function startDraw(e) {
e.preventDefault(); // 防止滚动
isDrawing = true;
var pos = getPos(e);
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
}

function moveDraw(e) {
if (!isDrawing) return;
e.preventDefault(); // 防止滚动
var pos = getPos(e);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
}

function endDraw(e) {
isDrawing = false;
}

// 绑定触摸事件
canvas.addEventListener('touchstart', startDraw, { passive: false });
canvas.addEventListener('touchmove', moveDraw, { passive: false });
canvas.addEventListener('touchend', endDraw);

// 兼容 PC 端鼠标调试
canvas.addEventListener('mousedown', startDraw);
canvas.addEventListener('mousemove', moveDraw);
canvas.addEventListener('mouseup', endDraw);
canvas.addEventListener('mouseleave', endDraw);


这里有几个细节需要你注意一下。

第一,事件监听里我加了 { passive: false },并且在代码里调用了 e.preventDefault()。这和你 CSS 里的 touch-action: none 是配套使用的,目的就是为了阻止手指画画时页面跟着滚动。

第二,getBoundingClientRect() 是获取位置的关键,它返回的 rect.left 是 Canvas 左边缘相对于视口的距离,用 clientX 减去它,就得到了手指相对于 Canvas 左上角的距离。

第三,如果你发现线条虽然位置对了,但是看起来有锯齿或者太细/太粗,那是 dpr(设备像素比)在作怪。我在 initCanvas 里处理了这个问题,通过 ctx.scale 缩放上下文,这样你写 lineWidth = 2,在手机和电脑上看起来粗细就是一样的,不会忽大忽小。

你按这个逻辑改一下,基本就能解决偏移问题了。如果还有问题,检查一下你的 Canvas 是不是被什么父级容器加了 transform 或者奇怪的定位,那也会影响 getBoundingClientRect 的计算值。
点赞 1
2026-03-03 23:22