Grant
Grant

Reputation: 451

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

I am working with an HTML canvas blown up 32x its pixel width and height. When I draw lines on it, however, I notice that the edge pixels of lines are drawn semi-transparently (exactly half transparent). Is there a way to stop this?

In this picture, the red line is a single line from one point to the other. I would like all of the blocks to be either black or #FF0000 red.

N.B. I am already using canvas.translate() to align pixels properly and am using the solution in this post to render the expanded pixels in discrete blocks.

Upvotes: 1

Views: 325

Answers (1)

user1693593
user1693593

Reputation:

Problem background

Canvas uses anti-aliasing to make drawings appear more smooth which is why it fills in semi-transparent pixels here and there (see this explanation for how that works).

Smoothing (aka interpolation) can be turned off, but only for images (ctx.imageSmoothingEnabled = false, as the name implies).

Solutions

For this a "line renderer" needs to be implemented. However, the typical line algorithms only supports lines of 1 pixel in width. This includes Bresenham as well as the EFLA (Extremely Fast Line Algorithm by Po-Han Lin), latter is faster than Bresenham.

For lines thicker than 1 pixel you would need to find tangent angle and then render each segment along the main line.

I provide both implementations below which I have optimized to some degree. None of them require access to the bitmap itself, just supply the context.

The only thing you need to remember is to use fillStyle (and fill()) instead of strokeStyle (and stroke()) to set its color. You can generate several lines before filling, which is generally faster than filling each line segment, provided they use the same color.

Optionally you could use image data and set pixels there directly, but that is slower and requires CORS in case you use images (use such a bitmap with a Uint32 view if this is preferred. There are also special tricks to speed up this approach, but they are not addressed here).

EFLA (Extremely Fast Line Algorithm)

This algorithm is intended where you want to draw continuous polygon lines, ie. the last point is not set. But in the following implementation we set it manually so it can be used for single line segments.

Visit the linked site above for a more in-depth explanation of it (as well as for license).

Just make sure input values are integer values:

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

Bresenham

This is a classic line algorithm used in many applications and computers in the old days where a simple line needed to be rendered.

The algorithm is explained more in detail 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; }
    }
}

Live demo including zoom

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>

Tip: These implementations can be optimized further by using a single rect() for vertical and horizontal lines (shown for Bresenham, not for EFLA). The setPixel() is for flexibility (f.ex. it can be rewritten to set a bitmap pixel instead etc.).

Upvotes: 4

Related Questions