问题描述
我试图在 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>
这是我想要的上面片段中缩放图像的结果:
编辑:请不要忽略翻译。
解决方法
给定的答案有很多幕后开销,获取和创建一个 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>