问题
最近使用 canvas 的时候遇到了一个令人苦恼的问题, 缩放后, 同样大小的 rect 却不能覆盖原有的 rect.
上面的图有一圈很淡的红色细线组成的矩形, 但是我是先后绘制两个一样大小的 rect, 并且这两个 rect 都是 xy 和 wh 都是整数.
const x = 50;
const y = 50;
const w = 1900;
const h = 50;
ctx.scale(2.2, 2.2);
ctx.translate(-51, 51)
// 绘制一个矩形,填充为红色
ctx.fillStyle = 'red';
ctx.fillRect(x, y, w, h);
// 在红色矩形上绘制一个白色矩形
ctx.fillStyle = 'white';
ctx.fillRect(x, y, w, h);
原因
由于这里设置了缩放 1.2, 我的屏幕 dpr = 2, 最终得到的 transform a 和 d 都是 2.4, 这不是整数. 且设置 scale 为 1.5, 问题将消失. 此刻 transform 的 a 和 d 都是 3, 恰好都是整数. 因为设置了缩放, 实际渲染的位置就是小数, 为了渲染, 实际物理像素将以灰阶的方式呈现, 因此部分像素是半透明效果.
但是缩放级别是用户决定的, 如何解决这个瑕疵呢?
尝试
问了下 GPT, 给出了 Math.round( val * zoom ) / zoom 这样的方式, 尝试看看
这个式子很好理解, 当 zoom 是整数的时候, 我们常用它来保留小数点后有效位.
但是此刻的情况 zoom 是小数, 如何理解呢? 目前看起来很迷惑.
假设 val = 51 并且 zoom = 2.4。
- 计算
val * zoom得到51 * 2.4 = 122.4。 - 使用
Math.round()对122.4进行四舍五入,得到122。 - 再将结果除以
zoom得到122 / 2.4 ≈ 50.83。
ctx.fillRectWithZoom = function (x, y, width, height) {
const m = this.getTransform();
const scaleX = m.a;
const scaleY = m.d;
x = Math.round(x * scaleX) / scaleX;
y = Math.round(y * scaleY) / scaleY;
width = Math.round(width * scaleX) / scaleX;
height = Math.round(height * scaleY) / scaleY;
this.fillRect(x, y, width, height);
}
ctx.translateWithZoom = function (x, y) {
const m = this.getTransform();
const scaleX = m.a;
const scaleY = m.d;
x = Math.round(x * scaleX) / scaleX;
y = Math.round(y * scaleY) / scaleY;
this.translate(x, y);
}
// problem
{
ctx.save();
// ctx.scale(1, 1);
ctx.scale(1.3, 1.3);
ctx.translate(51, 51) // bug
// 绘制一个矩形,填充为红色
ctx.fillStyle = 'red';
ctx.fillRect(x, y, w, h);
// 在红色矩形上绘制一个白色矩形
ctx.fillStyle = 'white';
ctx.fillRect(x, y, w, h);
ctx.restore();
}
// fix0 ok!
{
ctx.save();
ctx.scale(2.4, 2.4);
ctx.translateWithZoom(-50, 50)
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.fillRectWithZoom(x, y, w, h);
ctx.fillStyle = 'white';
ctx.fillRectWithZoom(x, y, w, h);
ctx.restore();
x = 50.1;
y = 51.4;
w = 190.2;
h = 50.3;
ctx.save();
ctx.scale(1.32, 1.31);
ctx.translateWithZoom(249.1, 11.1);
ctx.fillStyle = 'green';
ctx.beginPath();
ctx.fillRectWithZoom(x, y, w, h);
ctx.fillStyle = 'white';
ctx.fillRectWithZoom(x, y, w, h);
ctx.restore();
}
一顿操作下来, 竟然修复了,而且对于非整数矩形也能正确覆盖. 实测 translate 和 fillRect 都需处理 zoom.
尝试之2
查资料还看到另一种解法, 使用 setTransform 而不是 scale, 不过实测并不能适用所有的情形.
当 transform 是一位小数时, 大部分情况 OK. 但是两位小数就不行了, 而且 rect 的位置和大小是小数也不行.
{
x = 50;
y = 190;
w = 200;
h = 50;
ctx.setTransform(1.37, 0, 0, 1.37, 0, 0);
ctx.fillStyle = 'blue';
ctx.fillRect(x, y, w, h);
ctx.fillStyle = 'white';
ctx.fillRect(x, y, w, h);
}
尝试3
另一种尝试, 不太建议, 还原度不足而且需要重置 transform. 此方法是在绘制 rect 之前, 先将缩放回 1x, 然后再绘制, 不过 translate 和 rect 位置大小都需要取整, 否则还是无法完全覆盖.
ctx.fillRectWithZoom2 = function (x, y, width, height) {
const m = this.getTransform();
const scaleX = m.a;
const scaleY = m.d;
ctx.save();
ctx.setTransform(1, 0, 0, 1, Math.round(m.e * scaleX), Math.round(m.f * scaleY));
x = Math.round(x * scaleX);
y = Math.round(y * scaleY);
const w = Math.round(width * scaleX);
const h = Math.round(height * scaleY);
this.fillRect(x, y, w, h);
ctx.restore();
}
// test 3
{
x = 150.5;
y = 290.3;
w = 200;
h = 50;
ctx.setTransform(1.3, 0, 0, 1.3, 451, 151);
ctx.fillStyle = 'black';
ctx.fillRectWithZoom2(x, y, w, h);
ctx.fillStyle = 'white';
ctx.fillRectWithZoom2(x, y, w, h);
}
Demo