如何防止线条的边缘像素半透明?
How can I keep the edge pixels of lines from being semi-transparent?
我正在使用 HTML canvas 像素宽度和高度放大 32 倍的图像。但是,当我在其上绘制线条时,我注意到线条的边缘像素是半透明绘制的(恰好是半透明的)。有办法阻止吗?
在this picture中,红线是从一点到另一点的单线。我希望所有方块都是黑色或#FF0000 红色。
N.B。我已经在使用 canvas.translate() 正确对齐像素,并使用 this post 中的解决方案在离散块中渲染扩展像素。
问题背景
Canvas 使用抗锯齿使绘图看起来更平滑,这就是它在各处填充半透明像素的原因(请参阅 了解其工作原理)。
可以关闭平滑(又名插值),但仅限于图像(ctx.imageSmoothingEnabled = false
,顾名思义)。
解决方案
为此,需要实施 "line renderer"。然而,典型的线条算法只支持宽度为 1 像素的线条。这包括 Bresenham 以及 EFLA(Extremely Fast Line Algorithm by Po-Han Lin),后者比 Bresenham 更快。
对于大于 1 像素的线条,您需要找到切线角度,然后沿主线渲染每个线段。
我提供了两个实现,我在下面进行了一定程度的优化。 None 其中需要访问位图本身,只需提供上下文即可。
你唯一需要记住的是使用fillStyle
(和fill()
)而不是strokeStyle
(和stroke()
)来设置它的颜色。您可以在填充之前生成几条线,这通常比填充每个线段要快,前提是它们使用相同的颜色。
您可以选择使用图像数据并直接在那里设置像素,但这速度较慢,并且在您使用图像的情况下需要 CORS(如果首选,请使用带有 Uint32 视图的位图。还有一些特殊的技巧可以加快速度这种方法,但这里没有解决。
EFLA(极速线算法)
此算法适用于要绘制连续多边形线的地方,即。最后一点未设置。但是在下面的实现中我们手动设置它,所以它可以用于单线段。
访问上面的链接站点以获得更深入的解释(以及许可证)。
只需确保输入值为整数值:
function lineEFLA(ctx, x1, y1, x2, y2) {
var dlt, mul, yl = false, i,
sl = y2 - y1,
ll = x2 - x1,
lls = ll >> 31,
sls = sl >> 31;
if ((sl ^ sls) - sls > (ll ^ lls) - lls) {
sl ^= ll;
ll ^= sl;
sl ^= ll;
yl = true;
}
dlt = ll < 0 ? -1 : 1;
mul = (ll === 0) ? sl : sl / ll;
if (yl) {
x1 += 0.5; // preset for rounding
for (i = 0; i !== ll; i += dlt) setPixel((x1 + i * mul)|0, y1 + i);
}
else {
y1 += 0.5;
for (i = 0; i !== ll; i += dlt) setPixel(x1 + i, (y1 + i * mul)|0);
}
setPixel(x2, y2); // sets last pixel
function setPixel(x, y) {ctx.rect(x, y, 1, 1)}
}
布雷森汉姆
这是一个经典的线条算法,在过去需要渲染一条简单的线条的许多应用程序和计算机中使用。
更详细地解释了算法here。
function lineBresenham(ctx, x1, y1, x2, y2) {
if (x1 === x2) { // special case, vertical line
ctx.rect(x1, Math.min(y1, y2), 1, Math.abs(y2 - y1) + 1);
return;
}
if (y1 === y2) { // special case, horizontal line
ctx.rect(Math.min(x1, x2), y1, Math.abs(x2 - x1) + 1, 1);
return;
}
var dx = Math.abs(x2 - x1), sx = x1 < x2 ? 1 : -1,
dy = Math.abs(y2 - y1), sy = y1 < y2 ? 1 : -1,
err = (dx > dy ? dx : -dy) * 0.5;
while(!0) {
ctx.rect(x1, y1, 1, 1);
if (x1 === x2 && y1 === y2) break;
var e2 = err;
if (e2 > -dx) { err -= dy; x1 += sx; }
if (e2 < dy) { err += dx; y1 += sy; }
}
}
包括缩放的现场演示
var ctx = document.querySelector("canvas").getContext("2d");
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); // bg color
ctx.scale(20, 20); // scale
ctx.fillStyle = "#f00"; // color for line in this case
lineEFLA(ctx, 0, 0, 17, 20); // algo 1
lineBresenham(ctx, 3, 0, 20, 20); // algo 2
ctx.fill(); // fill the rects, use beginPath() for next
function lineEFLA(ctx, x1, y1, x2, y2) {
/* x1 |= 0; // make sure values are integer values
x2 |= 0;
y1 |= 0;
y2 |= 0;*/
var dlt,
mul,
sl = y2 - y1,
ll = x2 - x1,
yl = false,
lls = ll >> 31,
sls = sl >> 31,
i;
if ((sl ^ sls) - sls > (ll ^ lls) - lls) {
sl ^= ll;
ll ^= sl;
sl ^= ll;
yl = true;
}
dlt = ll < 0 ? -1 : 1;
mul = (ll === 0) ? sl : sl / ll;
if (yl) {
x1 += 0.5;
for (i = 0; i !== ll; i += dlt)
setPixel((x1 + i * mul) | 0, y1 + i);
} else {
y1 += 0.5;
for (i = 0; i !== ll; i += dlt)
setPixel(x1 + i, (y1 + i * mul) | 0);
}
setPixel(x2, y2); // sets last pixel
function setPixel(x, y) {
ctx.rect(x, y, 1, 1)
}
}
function lineBresenham(ctx, x1, y1, x2, y2) {
if (x1 === x2) { // special case, vertical line
ctx.rect(x1, Math.min(y1, y2), 1, Math.abs(y2 - y1) + 1);
return;
}
if (y1 === y2) { // special case, horizontal line
ctx.rect(Math.min(x1, x2), y1, Math.abs(x2 - x1) + 1, 1);
return;
}
var dx = Math.abs(x2 - x1),
sx = x1 < x2 ? 1 : -1,
dy = Math.abs(y2 - y1),
sy = y1 < y2 ? 1 : -1,
err = (dx > dy ? dx : -dy) * 0.5;
while (!0) {
ctx.rect(x1, y1, 1, 1);
if (x1 === x2 && y1 === y2) break;
var e2 = err;
if (e2 > -dx) {
err -= dy;
x1 += sx;
}
if (e2 < dy) {
err += dx;
y1 += sy;
}
}
}
<canvas width=400 height=400></canvas>
提示:可以通过对垂直线和水平线使用单个 rect() 来进一步优化这些实现(显示的是 Bresenham,而不是 EFLA)。 setPixel()
是为了灵活性(f.ex。它可以重写为设置位图像素等)。
我正在使用 HTML canvas 像素宽度和高度放大 32 倍的图像。但是,当我在其上绘制线条时,我注意到线条的边缘像素是半透明绘制的(恰好是半透明的)。有办法阻止吗?
在this picture中,红线是从一点到另一点的单线。我希望所有方块都是黑色或#FF0000 红色。
N.B。我已经在使用 canvas.translate() 正确对齐像素,并使用 this post 中的解决方案在离散块中渲染扩展像素。
问题背景
Canvas 使用抗锯齿使绘图看起来更平滑,这就是它在各处填充半透明像素的原因(请参阅
可以关闭平滑(又名插值),但仅限于图像(ctx.imageSmoothingEnabled = false
,顾名思义)。
解决方案
为此,需要实施 "line renderer"。然而,典型的线条算法只支持宽度为 1 像素的线条。这包括 Bresenham 以及 EFLA(Extremely Fast Line Algorithm by Po-Han Lin),后者比 Bresenham 更快。
对于大于 1 像素的线条,您需要找到切线角度,然后沿主线渲染每个线段。
我提供了两个实现,我在下面进行了一定程度的优化。 None 其中需要访问位图本身,只需提供上下文即可。
你唯一需要记住的是使用fillStyle
(和fill()
)而不是strokeStyle
(和stroke()
)来设置它的颜色。您可以在填充之前生成几条线,这通常比填充每个线段要快,前提是它们使用相同的颜色。
您可以选择使用图像数据并直接在那里设置像素,但这速度较慢,并且在您使用图像的情况下需要 CORS(如果首选,请使用带有 Uint32 视图的位图。还有一些特殊的技巧可以加快速度这种方法,但这里没有解决。
EFLA(极速线算法)
此算法适用于要绘制连续多边形线的地方,即。最后一点未设置。但是在下面的实现中我们手动设置它,所以它可以用于单线段。
访问上面的链接站点以获得更深入的解释(以及许可证)。
只需确保输入值为整数值:
function lineEFLA(ctx, x1, y1, x2, y2) {
var dlt, mul, yl = false, i,
sl = y2 - y1,
ll = x2 - x1,
lls = ll >> 31,
sls = sl >> 31;
if ((sl ^ sls) - sls > (ll ^ lls) - lls) {
sl ^= ll;
ll ^= sl;
sl ^= ll;
yl = true;
}
dlt = ll < 0 ? -1 : 1;
mul = (ll === 0) ? sl : sl / ll;
if (yl) {
x1 += 0.5; // preset for rounding
for (i = 0; i !== ll; i += dlt) setPixel((x1 + i * mul)|0, y1 + i);
}
else {
y1 += 0.5;
for (i = 0; i !== ll; i += dlt) setPixel(x1 + i, (y1 + i * mul)|0);
}
setPixel(x2, y2); // sets last pixel
function setPixel(x, y) {ctx.rect(x, y, 1, 1)}
}
布雷森汉姆
这是一个经典的线条算法,在过去需要渲染一条简单的线条的许多应用程序和计算机中使用。
更详细地解释了算法here。
function lineBresenham(ctx, x1, y1, x2, y2) {
if (x1 === x2) { // special case, vertical line
ctx.rect(x1, Math.min(y1, y2), 1, Math.abs(y2 - y1) + 1);
return;
}
if (y1 === y2) { // special case, horizontal line
ctx.rect(Math.min(x1, x2), y1, Math.abs(x2 - x1) + 1, 1);
return;
}
var dx = Math.abs(x2 - x1), sx = x1 < x2 ? 1 : -1,
dy = Math.abs(y2 - y1), sy = y1 < y2 ? 1 : -1,
err = (dx > dy ? dx : -dy) * 0.5;
while(!0) {
ctx.rect(x1, y1, 1, 1);
if (x1 === x2 && y1 === y2) break;
var e2 = err;
if (e2 > -dx) { err -= dy; x1 += sx; }
if (e2 < dy) { err += dx; y1 += sy; }
}
}
包括缩放的现场演示
var ctx = document.querySelector("canvas").getContext("2d");
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); // bg color
ctx.scale(20, 20); // scale
ctx.fillStyle = "#f00"; // color for line in this case
lineEFLA(ctx, 0, 0, 17, 20); // algo 1
lineBresenham(ctx, 3, 0, 20, 20); // algo 2
ctx.fill(); // fill the rects, use beginPath() for next
function lineEFLA(ctx, x1, y1, x2, y2) {
/* x1 |= 0; // make sure values are integer values
x2 |= 0;
y1 |= 0;
y2 |= 0;*/
var dlt,
mul,
sl = y2 - y1,
ll = x2 - x1,
yl = false,
lls = ll >> 31,
sls = sl >> 31,
i;
if ((sl ^ sls) - sls > (ll ^ lls) - lls) {
sl ^= ll;
ll ^= sl;
sl ^= ll;
yl = true;
}
dlt = ll < 0 ? -1 : 1;
mul = (ll === 0) ? sl : sl / ll;
if (yl) {
x1 += 0.5;
for (i = 0; i !== ll; i += dlt)
setPixel((x1 + i * mul) | 0, y1 + i);
} else {
y1 += 0.5;
for (i = 0; i !== ll; i += dlt)
setPixel(x1 + i, (y1 + i * mul) | 0);
}
setPixel(x2, y2); // sets last pixel
function setPixel(x, y) {
ctx.rect(x, y, 1, 1)
}
}
function lineBresenham(ctx, x1, y1, x2, y2) {
if (x1 === x2) { // special case, vertical line
ctx.rect(x1, Math.min(y1, y2), 1, Math.abs(y2 - y1) + 1);
return;
}
if (y1 === y2) { // special case, horizontal line
ctx.rect(Math.min(x1, x2), y1, Math.abs(x2 - x1) + 1, 1);
return;
}
var dx = Math.abs(x2 - x1),
sx = x1 < x2 ? 1 : -1,
dy = Math.abs(y2 - y1),
sy = y1 < y2 ? 1 : -1,
err = (dx > dy ? dx : -dy) * 0.5;
while (!0) {
ctx.rect(x1, y1, 1, 1);
if (x1 === x2 && y1 === y2) break;
var e2 = err;
if (e2 > -dx) {
err -= dy;
x1 += sx;
}
if (e2 < dy) {
err += dx;
y1 += sy;
}
}
}
<canvas width=400 height=400></canvas>
提示:可以通过对垂直线和水平线使用单个 rect() 来进一步优化这些实现(显示的是 Bresenham,而不是 EFLA)。 setPixel()
是为了灵活性(f.ex。它可以重写为设置位图像素等)。