如何计算偏移量以绘制到应用了缩放变换的画布中最近的像素?

问题描述

我试图在 HTML 画布上绘制一个具有像素完美定位的矩形,而不管应用于画布变换的平移和缩放(假设在这种情况下不使用旋转)。

在这种情况下,我总是希望矩形的边框在屏幕上为 1px 宽,无论矩形在应用比例的情况下如何放大(我在下面的演示中工作),但是我也想要它以精确的像素坐标绘制,因此没有应用抗锯齿。

在下面的代码中,我很确定我必须操纵 ctx.rect 的参数来为每个位置添加偏移量,以便将其四舍五入以准确显示在屏幕上最近的像素上。但我不确定要使用什么数学方法才能达到目标?

正如您在此演示中所见,应用 1.5 缩放比例后,矩形不再绘制在像素完美坐标上。

const canvases = [
  {
    ctx: document.getElementById('canvasOriginal').getContext('2d'),scale: 1,translateX: 0,translateY: 0
  },{
    ctx: document.getElementById('canvasZoomed').getContext('2d'),scale: 1.5,translateX: -0.5,translateY: -0.5
  }
];

for (const { ctx,scale,translateX,translateY } of canvases) {
  ctx.translate(translateX,translateY);
  ctx.scale(scale,scale);
  ctx.beginPath();
  ctx.rect(1.5,1.5,4,4);
  ctx.linewidth = 1 / scale;
  ctx.strokeStyle = 'red';
  ctx.stroke();
}
canvas {
  border: 1px solid #ccc;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  width: 100px;
  height: 100px;
}
<canvas id="canvasOriginal" width="10" height="10"></canvas>
<canvas id="canvasZoomed" width="10" height="10"></canvas>

这是我想要的上面片段中缩放图像的结果:

enter image description here

编辑:请不要忽略翻译。

解决方法

给定的答案有很多幕后开销,获取和创建一个 DOM 矩阵,创建一个 DOM 点,转换该点然后反转矩阵并转换回来是文字 100+ 乘法和加法(忽略内存管理开销)。

由于您只是缩放而没有平移或旋转,因此可以在一个除法、八个乘法和八个加法/减法中完成,并且至少快一个数量级。

示例

const canvases = [
  { ctx: document.getElementById('canvasOriginal').getContext('2d'),scale: 1 },{ ctx: document.getElementById('canvasZoomed').getContext('2d'),scale: 1.5 },];
function pathRectPixelAligned(ctx,scale,x,y,w,h) {
    const invScale = 1 / scale;
    x = (Math.round(x * scale) + 0.5) * invScale ;   
    y = (Math.round(y * scale) + 0.5) * invScale ;  
    w = (Math.round((x + w) * scale) - 0.5) * invScale - x;   
    h = (Math.round((y + h) * scale) - 0.5) * invScale - y; 
   ctx.rect(x,h);
}
for (const { ctx,scale } of canvases) {
  ctx.scale(scale,scale);
  ctx.beginPath();
  pathRectPixelAligned(ctx,1,4,4)
  ctx.lineWidth = 1;
  ctx.strokeStyle = 'red';
  ctx.setTransform(1,0);
  ctx.stroke();
}
canvas {
  border: 1px solid #ccc;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  width: 100px;
  height: 100px;
}
<canvas id="canvasOriginal" width="10" height="10"></canvas>
<canvas id="canvasZoomed" width="10" height="10"></canvas>

,

好的,我呃……我自己弄明白了。您可以使用 DOMPoint 类通过画布的变换矩阵来变换坐标。所以我写了一个函数,通过矩阵转换点,将其四舍五入到最近的半像素(因为 1 像素宽的笔划在中心呈现,例如像素的半点),然后通过逆矩阵。

这会导致将缩放的 1 像素笔划渲染到屏幕上最近的像素。

希望这个问题对浏览互联网的其他人有用,因为在发布这个问题之前我花了很长时间才弄清楚这个问题......

const canvases = [
  {
    ctx: document.getElementById('canvasOriginal').getContext('2d'),scale: 1,translateX: 0,translateY: 0
  },{
    ctx: document.getElementById('canvasZoomed').getContext('2d'),scale: 1.5,translateX: -0.5,translateY: -0.5
  }
];

const roundPointToHalfIdentityCoordinates = (ctx,y) => {
  let point = new DOMPoint(x,y);
  point = point.matrixTransform(ctx.getTransform());
  point.x = Math.round(point.x - 0.5) + 0.5;
  point.y = Math.round(point.y - 0.5) + 0.5;
  point = point.matrixTransform(ctx.getTransform().inverse());
  return point;
};

for (const { ctx,translateX,translateY } of canvases) {
  ctx.translate(translateX,translateY);
  ctx.scale(scale,scale);
  ctx.beginPath();
  const topLeft = roundPointToHalfIdentityCoordinates(ctx,1.5,1.5);
  const bottomRight = roundPointToHalfIdentityCoordinates(ctx,5.5,5.5);
  ctx.rect(
    topLeft.x,topLeft.y,bottomRight.x - topLeft.x,bottomRight.y - topLeft.y
  );
  ctx.lineWidth = 1 / scale;
  ctx.strokeStyle = 'red';
  ctx.stroke();
}
canvas {
  border: 1px solid #ccc;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  width: 100px;
  height: 100px;
}
<canvas id="canvasOriginal" width="10" height="10"></canvas>
<canvas id="canvasZoomed" width="10" height="10"></canvas>