如何制作一个快速的非抗锯齿HTML5canvas基本绘图功能? [英] how to make a fast NOT anti-aliasing HTML5canvas basic drawing function?

查看:98
本文介绍了如何制作一个快速的非抗锯齿HTML5canvas基本绘图功能?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图在画布上做一个抗锯齿绘图功能。
感谢本网站上关于画布和别名的所有超级答案。

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

这是演示: https:// jsfiddle.net/garciaVvV/eu34c8sy/12/

这里是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?

谢谢

推荐答案

主要原因当然是生成了相当多的路径,首先是圆形,然后是线条,它再现圆形路径x每个像素的长度。

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.

在我们工作的环境中,使用Bresenham绘制和填充一条线可能会非常慢。多次绘制圆圈的速度也很慢。

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 :) ).

所以让我们试试第三个选项。我们既需要内部线路机制,也需要Bresenham。挑战在于让Bresenham完全覆盖边缘。

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>

让我们看看实际上,添加 Bresenham 可以使用更快的线算法: EFLA 并尝试匹配边缘 - 现在,这可能并非在所有情况下都是完美的,并且可能必须调整偏移(或者原始绘制操作的线宽)。

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.

我们还需要计算两侧角度的90°偏移。而不是加减90°我们可以改为换算cos / sin。

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>

一些最终说明:请注意这可能不是完美的,别名的,特别是在几乎0/90°线。这是因为由于样本数量的原因,EFLA线无法用单像素点覆盖许多点,从而形成一条精细的渐变线。

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.

这篇关于如何制作一个快速的非抗锯齿HTML5canvas基本绘图功能?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆