如何使线条的边缘像素保持半透明

How can I keep the edge pixels of lines from being semi-transparent?

本文关键字:半透明 边缘像素 何使线      更新时间:2023-09-26

我正在处理一个HTML画布,它的像素宽度和高度是它的32倍。然而,当我在上面画线时,我注意到线的边缘像素是半透明的(完全半透明)。有办法阻止这种情况吗?

在这张图中,红线是从一点到另一点的一条直线。我希望所有的方块都是黑色或#FF0000红色。

注意:我已经在使用canvas.translate()来正确对齐像素,并使用本文中的解决方案来渲染离散块中的扩展像素。

问题背景

Canvas使用抗锯齿使图形看起来更平滑,这就是为什么它到处填充半透明像素的原因(请参阅此解释了解其工作原理)。

平滑(也称为插值)可以关闭,但仅适用于图像(顾名思义,ctx.imageSmoothingEnabled = false)。

解决方案

为此,需要实现"线渲染器"。然而,典型的线条算法仅支持宽度为1像素的线条。这包括Bresenham和EFLA(林宝涵的极快线算法),后者比Bresenham更快。

对于大于1像素的线,您需要找到切线角度,然后沿主线渲染每个线段。

我提供了两种实现,在这两种实现之下,我已经在一定程度上进行了优化。它们都不需要访问位图本身,只需提供上下文即可。

您唯一需要记住的是使用fillStyle(和fill())而不是strokeStyle(和stroke())来设置其颜色。在填充之前可以生成多条线,这通常比填充每条线段更快,前提是它们使用相同的颜色。

您可以选择使用图像数据并直接在那里设置像素,但速度较慢,并且在使用图像的情况下需要CORS(如果首选,请使用带有Uint32视图的位图。也有一些特殊技巧可以加快这种方法的速度,但此处未提及)。

极快速直线算法

此算法适用于要绘制连续多边形线的位置,即未设置最后一个点的位置。但在下面的实现中,我们手动设置它,以便它可以用于单条线段。

访问上面的链接网站以获得更深入的解释(以及许可证)。

只需确保输入值是整数值:

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)}
}

布雷森汉姆

这是一种经典的线条算法,在过去的许多应用程序和计算机中都需要渲染一条简单的线条。

这里将更详细地解释该算法。

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()是为了灵活性(例如,它可以重写以设置位图像素等)。