how to make a fast NOT anti-aliasing HTML5canvas b

2019-08-23 00:45发布

I trying to do an anti-aliasing drawing function in canvas. Thanks to all the super answers on this site about canvas and aliasing.

here's the demo: https://jsfiddle.net/garciaVvV/eu34c8sy/12/

here's the line js:

function lineXY(mouseX, mouseY, mouseXX, mouseYY){
  var x0= mouseX;
  var y0= mouseY;
  var x1= mouseXX;
  var y1= mouseYY;

var coordinatesArray = [];
// Translate coordinates
// Define differences and error check
var dx = Math.abs(x1 - x0);
var dy = Math.abs(y1 - y0);
var sx = (x0 < x1) ? 1 : -1;
var sy = (y0 < y1) ? 1 : -1;
var err = dx - dy;
// Set first coordinates
coordinatesArray.push([x0,y0]);

// Main loop
while (!((x0 == x1) && (y0 == y1))) {
  var e2 = err << 1;
  if (e2 > -dy) {
    err -= dy;
    x0 += sx;
  }
  if (e2 < dx) {
    err += dx;
    y0 += sy;
  }
  // Set coordinates
coordinatesArray.push([x0,y0]);
  // Return the result
}

 for(var i=0;i<coordinatesArray.length;i++) {
   aliasedCircle(ctx, coordinatesArray[i][0], coordinatesArray[i][1], 100);
 }
}

What make it jerky while drawing fast with a large pen ? And how to make it sweet?

Thanks

1条回答
姐就是有狂的资本
2楼-- · 2019-08-23 01:04

The main reason is of course that quite a large number of paths are generated, first with the circle and then with the line which reproduces the circle paths x length per pixel.

There are a couple of things we can do to improve this:

  • We can cache the circle as an image and use it as a bitmap brush. This eliminates the need to regenerate all the lines in the circle for each point in the line. The brush only needs to be updated when size or color changes.

  • We don't have to draw each point of the line, we can find a way to calculate how many pixels we can skip before we need to draw, but an better option is:

  • We can "cheat" by drawing a thick line between the first and last point instead of drawing a circle each point.

  • And finally, we can register mouse on each frame instead of each event to reduce the load.

The first point is simple enough: simply create an offscreen canvas the size of the brush (diameter) and draw in. To change color either regenerate brush (or use composite mode and draw over it):

// show brush
document.body.appendChild(createBrush(150, "#09f"));

function createBrush(radius, color) {
  var ctx = document.createElement("canvas").getContext("2d");
  ctx.canvas.width = ctx.canvas.height = radius<<1; 
  ctx.fillStyle = color;            
  aliasedCircle(ctx, radius, radius, radius);
  ctx.fill();                  
  return ctx.canvas
}

function aliasedCircle(ctx, xc, yc, r) {   // NOTE: for fill only!
  var x = r, y = 0, cd = 0;

  // middle line
  ctx.rect(xc - x, yc, r<<1, 1);

  while (x > y) {
    cd -= (--x) - (++y);
    if (cd < 0) cd += x++;
    ctx.rect(xc - y, yc - x, y<<1, 1);  // upper 1/4
    ctx.rect(xc - x, yc - y, x<<1, 1);  // upper 2/4
    ctx.rect(xc - x, yc + y, x<<1, 1);  // lower 3/4
    ctx.rect(xc - y, yc + x, y<<1, 1);  // lower 4/4
  }
}

Now that we have an image/bitmap brush we can look at how to draw the line. We can use two approaches. Since you want it aliased we have to compromise somehow.

Using a Bresenham to draw and fill a line can be very slow in the context we're working. Drawing the circle multiple times is slow as well.

An third option is to use the context's own line and "hack" the edges (of course, if all this is to improve filling with bucket fill, ref. previous question, I would probably spend the energy on improving the bucket fill algorithm instead :) ).

So lets try the third option. We need both the internal line mechanism as well as the Bresenham. The challenge is to make the Bresenham cover the edge exactly.

var ctx = c.getContext("2d");

drawLine(ctx, 60, 60, 250, 210, 50);
ctx.stroke();

function drawLine(ctx, x1, y1, x2, y2, radius) {
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.lineWidth = radius<<1;
  ctx.lineCap = "butt";
}
<canvas id=c height=300></canvas>

Lets add Bresenham, actually, lets use a faster line algorithm: EFLA and try to match the edges - now, this may not be perfect in all cases and the offset (or rather line width of the native draw op.) may have to be adjusted.

We also needs to calculate 90° offset to the angle for both side. Instead of adding and subtracting 90° we can switch cos/sin instead.

var ctx = c.getContext("2d");
var x1 = 60, y1 = 60, x2 = 250, y2 = 210, r = 50;

ctx.globalAlpha = 0.25;
drawLine(ctx, x1, y1, x2, y2, r);
ctx.stroke();
ctx.beginPath();
ctx.globalAlpha = 1;

// calc angle
var diffX = x2 - x1,
    diffY = y2 - y1,
    angle = Math.atan2(diffY, diffX);

// two edge lines offset per angle
var lx1 = x1 - r * Math.sin(angle),
    ly1 = y1 + r * Math.cos(angle),
    lx2 = x2 - r * Math.sin(angle),
    ly2 = y2 + r * Math.cos(angle),
    rx1 = x1 + r * Math.sin(angle),
    ry1 = y1 - r * Math.cos(angle),
    rx2 = x2 + r * Math.sin(angle),
    ry2 = y2 - r * Math.cos(angle);

fastLine(ctx, lx1|0, ly1|0, lx2|0, ly2|0);
fastLine(ctx, rx1|0, ry1|0, rx2|0, ry2|0);
ctx.fill();

function drawLine(ctx, x1, y1, x2, y2, radius) {
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.lineWidth = radius<<1;
  ctx.lineCap = "butt";
}

function fastLine(ctx, x1, y1, x2, y2) {
  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)
      ctx.rect((x1 + i * mul)|0, y1 + i, 1, 1)
  }
  else {
    y1 += 0.5;
    for (i = 0; i !== ll; i += dlt)
      ctx.rect(x1 + i, (y1 + i * mul)|0, 1, 1)
  }
}
<canvas id=c height=300></canvas>

And finally, if we merge the components and refactor a little we get a neat aliased line drawing mechanism that utilizes these approaches:

var ctx = c.getContext("2d");
var x1 = 0, y1 = 0, r = 90;
var brush = createBrush(r, "#000");

document.querySelector("button").onclick = function() {
  ctx.beginPath();
  ctx.clearRect(0,0,c.width,c.height);
};

// mouse move handler using rAF.
c.onmousemove = function(e) {
  requestAnimationFrame(function() {
    var x2 = e.clientX|0, y2=e.clientY|0;
    aliasedLine(ctx, x1, y1, x2, y2, r);
    x1 = x2;
    y1 = y2;
  })
};

function aliasedLine(ctx, x1, y1, x2, y2, radius) {
  // calc angle
  var diffX = x2 - x1,
      diffY = y2 - y1,
      angle = Math.atan2(diffY, diffX),

      // two edge lines offset per angle
      lx1 = x1 - radius * Math.sin(angle),
      ly1 = y1 + radius * Math.cos(angle),
      lx2 = x2 - radius * Math.sin(angle),
      ly2 = y2 + radius * Math.cos(angle),
      rx1 = x1 + radius * Math.sin(angle),
      ry1 = y1 - radius * Math.cos(angle),
      rx2 = x2 + radius * Math.sin(angle),
      ry2 = y2 - radius * Math.cos(angle);

  // main line
  ctx.beginPath();
  drawLine(ctx, x1, y1, x2, y2, radius);
  ctx.stroke();
  
  // aliased edges
  ctx.beginPath();
  fastLine(ctx, lx1|0, ly1|0, lx2|0, ly2|0);
  fastLine(ctx, rx1|0, ry1|0, rx2|0, ry2|0);
  ctx.fill();

  // caps
  ctx.drawImage(brush, x1 - radius, y1 - radius)
  ctx.drawImage(brush, x2 - radius, y2 - radius)
}


function createBrush(radius, color) {
  var ctx = document.createElement("canvas").getContext("2d");
  ctx.canvas.width = ctx.canvas.height = 1 + radius<<1; 
  ctx.fillStyle = color;            
  aliasedCircle(ctx, radius, radius, radius);
  ctx.fill();                  
  return ctx.canvas
}

function aliasedCircle(ctx, xc, yc, r) {   // NOTE: for fill only!
  var x = r, y = 0, cd = 0;

  // middle line
  ctx.rect(xc - x, yc, r<<1, 1);

  while (x > y) {
    cd -= (--x) - (++y);
    if (cd < 0) cd += x++;
    ctx.rect(xc - y, yc - x, y<<1, 1);  // upper 1/4
    ctx.rect(xc - x, yc - y, x<<1, 1);  // upper 2/4
    ctx.rect(xc - x, yc + y, x<<1, 1);  // lower 3/4
    ctx.rect(xc - y, yc + x, y<<1, 1);  // lower 4/4
  }
}

function drawLine(ctx, x1, y1, x2, y2, radius) {
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.lineWidth = radius<<1;
}

function fastLine(ctx, x1, y1, x2, y2) {
  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)
      ctx.rect((x1 + i * mul)|0, y1 + i, 1, 1)
  }
  else {
    y1 += 0.5;
    for (i = 0; i !== ll; i += dlt)
      ctx.rect(x1 + i, (y1 + i * mul)|0, 1, 1)
  }
}
#c {background:#aaa}
<canvas id=c width=1200 height=800></canvas>
<br><button>Clear</button>

Some final notes: Just be aware of that it may not be perfect, alias-wise, in particular in almost 0/90° lines. This is because due to number of samples there can sit many points making a fine gradual line the EFLA line cannot cover with its single pixel point.

One alternative is to make a polygon fill (like scanline) implementation. It's a little more math and steps involved but doable with acceptable performance.

查看更多
登录 后发表回答