javascript - 更快的 HTML Canvas Bresenham 线

标签 javascript canvas html5-canvas 2d

渲染缓慢

我正在使用 Bresenham's line algorithm实时渲染像素艺术线条。它一次渲染 1 个像素 ctx.rect(x,y,1,1),这是一个缓慢的操作。我不能使用像素缓冲区,这会大大减少渲染开销,因为我正在使用复合操作、alpha 和过滤器(其中一些会污染 Canvas )。

函数

function pixelArtLine(ctx, x1, y1, x2, y2) {
    x1 = Math.round(x1);
    y1 = Math.round(y1);
    x2 = Math.round(x2);
    y2 = Math.round(y2);
    const dx = Math.abs(x2 - x1);
    const sx = x1 < x2 ? 1 : -1;
    const dy = -Math.abs(y2 - y1);
    const sy = y1 < y2 ? 1 : -1;
    var e2, er = dx + dy, end = false;
    ctx.beginPath();
    while (!end) {
        ctx.rect(x1, y1, 1, 1);
        if (x1 === x2 && y1 === y2) {
            end = true;
        } else {
            e2 = 2 * er;
            if (e2 > dy) {
                er += dy;
                x1 += sx;
            }
            if (e2 < dx) {
                er += dx;
                y1 += sy;
            }
        }
    }
    ctx.fill();        
};

如何改进此功能?

最佳答案

既然你在做像素艺术,为什么不在像素级别上做呢:直接操作 ImageData。

您声明您的 Canvas 很可能已被污染,并且将设置过滤器和 gCO。这些都不重要。

使用第二个屏幕外 Canvas ,仅用于生成像素艺术效果图。 将其大小设置为您渲染的像素网格之一(即 originalCanvasSize/pixelSize)。
直接在屏幕外的 ImageData 上执行数学运算。
将 ImageDaat 放在屏幕外 Canvas 上 使用 gCO 设置像素艺术颜色。 使用 drawImage 在渲染的 Canvas 上拉回屏幕外 Canvas 没有图像平滑(imageSmoothingEnbaled = false)。

您希望应用于路径绘图的过滤器和 gCO 也将应用于此最终 drawImage(offscreenCanvas)

我相信您将能够以更简洁的方式重写它,但这里是一个粗略的概念证明:

class PixelArtDrawer {
  constructor(ctx, options = {}) {
    if (!(ctx instanceof CanvasRenderingContext2D)) {
      throw new TypeError('Invalid Argument 1, not a canvas 2d context');
    }
    this.cursor = {
      x: 0,
      y: 0
    };
    this.strokeStyle = '#000';
    this.renderer = ctx;
    this.ctx = document.createElement('canvas').getContext('2d');
    this.setPixelSize((options && options.pixelSize) || 10);
  }
  setPixelSize(pixelSize) {
    this.pixelSize = pixelSize;
    const ctx = this.ctx;
    const canvas = ctx.canvas;
    const renderer = this.renderer.canvas;

    canvas.width = (renderer.width / pixelSize) | 0;
    canvas.height = (renderer.height / pixelSize) | 0;
    ctx.globalCompositeOperation = 'source-in';
    this.image = ctx.createImageData(canvas.width, canvas.height);
    this.data = new Uint32Array(this.image.data.buffer);
  }
  beginPath() {
    this.data.fill(0);
    this.cursor.x = this.cursor.y = null;
  }
  stroke() {
    const renderer = this.renderer
    const currentSmoothing = renderer.imageSmoothingEnbaled;
    const ctx = this.ctx;
    ctx.putImageData(this.image, 0, 0);
    // put the color
    ctx.fillStyle = this.strokeStyle;
    ctx.fillRect(0, 0, this.image.width, this.image.height);
    renderer.imageSmoothingEnabled = false;
    renderer.drawImage(ctx.canvas, 0, 0, renderer.canvas.width, renderer.canvas.height);
    renderer.imageSmoothingEnabled = currentSmoothing;
  }
  moveTo(x, y) {
    this.cursor.x = (x / this.pixelSize) | 0;
    this.cursor.y = (y / this.pixelSize) | 0;
  }
  lineTo(x, y) {
    if (this.cursor.x === null) {
      this.moveTo(x, y);
      return;
    }
    const data = this.data;
    const width = this.image.width;
    const height = this.image.height;
    var x1 = this.cursor.x;
    var y1 = this.cursor.y;

    const x2 = (x / this.pixelSize) | 0;
    const y2 = (y / this.pixelSize) | 0;
    // from here it is OP's code
    const dx = Math.abs(x2 - x1);
    const sx = x1 < x2 ? 1 : -1;
    const dy = -Math.abs(y2 - y1);
    const sy = y1 < y2 ? 1 : -1;
    var e2, er = dx + dy,
      end = false;
    var index;
    while (!end) {
      // this check would probably be better done out of the loop
      if (x1 >= 0 && x1 <= width && y1 >= 0 && y1 <= height) {
        // here we need to convert x, y coords to array index
        index = ((y1 * width) + x1) | 0;
        data[index] = 0xff000000;
      }
      if (x1 === x2 && y1 === y2) {
        end = true;
      } else {
        e2 = 2 * er;
        if (e2 > dy) {
          er += dy;
          x1 += sx;
        }
        if (e2 < dx) {
          er += dx;
          y1 += sy;
        }
      }
    }
    this.cursor.x = x2;
    this.cursor.y = y2;
  }
}
const ctx = renderer.getContext('2d');
const pixelArt = new PixelArtDrawer(ctx);
const points = [{
  x: 0,
  y: 0
}, {
  x: 0,
  y: 0
}];

draw();

renderer.onmousemove = function(e) {
  const rect = this.getBoundingClientRect();
  const lastPoint = points[points.length - 1];
  lastPoint.x = e.clientX - rect.left;
  lastPoint.y = e.clientY - rect.top;
};
renderer.onclick = e => {
  const lastPoint = points[points.length - 1];
  points.push({
    x: lastPoint.x,
    y: lastPoint.y
  });
};

function draw() {
  ctx.clearRect(0, 0, renderer.width, renderer.height);
  pixelArt.beginPath();
  points.forEach(drawLine);
  pixelArt.stroke();
  requestAnimationFrame(draw);
}

function drawLine(pt) {
  pixelArt.lineTo(pt.x, pt.y);
}
color_picker.onchange = function() {
  pixelArt.strokeStyle = this.value;
}
<input type="color" id="color_picker"><br>
<canvas id="renderer" width="500" height="500"></canvas>


关于javascript - 更快的 HTML Canvas Bresenham 线,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50625903/

相关文章:

javascript - 尝试使用 javascript camgaze.js 绘制图像,从网络摄像头进行实时人脸检测 -- 不起作用

javascript - 绘制图像轮廓的最佳实践

javascript - 通过多个属性比较两个对象

javascript - 模块.exports : is not a function

javascript - 删除 paper.js 中的路径?

python - 如何让 tkinter Canvas 动态调整为窗口宽度?

javascript - 检测 HTML canvas 元素的 mouseMove out 事件

javascript - 使表格单元格响应并使它们均匀正方形,填充窗口

javascript - jquery-ui 限制可选择项目的数量

javascript - Fabric JS 用相同的项目(ID)替换对象