首页 > 解决方案 > 使用 drawImage 缩放上面的画布

问题描述

我正在尝试使用 2 个画布制作像素编辑器。第一个画布显示包含像素的第二个画布。第一个画布使用drawImage来定位和缩放第二个画布。

当第二个画布被缩放到小于它的原始大小时,它开始出现故障。

这是以原始尺寸显示的画布。当我放大时,第二个画布变大了,一切都很完美。

正常规模

但是,当我缩小时,网格和背景(透明度)的行为非常奇怪。

规模较小

规模更小

要在第一个画布上绘制第二个画布,我使用函数

ctx.drawImage(drawCanvas, offset.x, offset.y, width * pixelSize, height * pixelSize);

我已经读过多次迭代中的缩放可能会提供更好的图像质量,但我不确定画布。

当用户缩小时,我可以以较低的分辨率完全重绘第二个画布,但这对 cpu 来说有点重。

有没有我不知道的更好的解决方案?

标签: javascripthtmlcsscanvas

解决方案


您的问题来自anti-aliasing

像素是不可分割的,当你要求计算机在像素边界之外绘制一些东西时,它会尽最大努力通过混合颜色来渲染通常看起来不错的东西,这样本来应该是黑色的例如0.1像素线会变成浅灰色像素。

这通常效果很好,特别是对于真实单词的图片或圆形等复杂形状。但是对于网格......这并不像你所经历的那么好。

您的案件正在处理两个不同的案件,您将不得不分别处理下摆。

  • 在画布 2D API(和许多 2D API)stroke中,确实会从您设置的坐标的两侧流血。因此,当绘制 1px 宽的线条时,您需要考虑 0.5px 的偏移量,以确保它不会被渲染为两个灰色像素。有关此的更多信息,请参阅此答案。您可能正在为网格使用这样的笔划。

  • fill另一方面,只覆盖形状的内部,所以如果你填充一个矩形,你不需要px 边界偏移它的坐标。这是棋盘所必需的。

现在,对于这些图纸,最好的可能是使用模式。你只需要画一个小版本,然后图案会自动重复,节省了大量的计算量。

可以通过调用 2D 上下文的变换方法来完成图案的缩放。我们甚至可以通过将imageSmoothingEnabled属性设置为 false来利用最近邻算法来避免在绘制此模式时进行抗锯齿。

但是对于我们的网格,我们可能希望保持 lineWidth 不变。为此,我们需要在每次绘制调用时生成一个新模式。

// An helper function to create CanvasPatterns
// returns a 2DContext on which a simple `finalize` method is attached
// method which does return a CanvasPattern from the underlying canvas
function patternMaker(width, height) {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  ctx.finalize = (repetition = "repeat") => ctx.createPattern(canvas, repetition);
  return ctx;
}

// The checkerboard can be generated only once
const checkerboard_patt_maker = patternMaker(2, 2);
checkerboard_patt_maker.fillStyle = "#CCC";
checkerboard_patt_maker.fillRect(0,0,1,1);
checkerboard_patt_maker.fillRect(1,1,1,1);
const checkerboard_patt = checkerboard_patt_maker.finalize();

// An helper function to create grid patterns
// Since we want a constant lineWidth, no matter the zoom level
function makeGridPattern(width, height) {
  width = Math.round(width);
  height = Math.round(height);
  const grid_patt_maker = patternMaker(width, height);
  grid_patt_maker.lineWidth = 1;
  // apply the 0.5 offset only if we are on integer coords
  // for instance a <3,3> pattern wouldn't need any offset, 1.5 is already perfect
  const x = width/2 % 1 ? width/2 : width/2 + 0.5;
  const y = height/2 % 1 ? height/2 : height/2 + 0.5;
  grid_patt_maker.moveTo(x, 0);
  grid_patt_maker.lineTo(x, height);
  grid_patt_maker.moveTo(0, y);
  grid_patt_maker.lineTo(width, y);
  grid_patt_maker.stroke();
  return grid_patt_maker.finalize();
}

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const checkerboard_input = document.getElementById('checkerboard_input');
const grid_input = document.getElementById('grid_input');
const connector = document.getElementById('connector');

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  const checkerboard_zoom = checkerboard_input.value;
  const grid_zoom = grid_input.value;
  // we generate a new pattern for the grid, so the lineWidth is always 1
  const grid_patt = makeGridPattern(grid_zoom,  grid_zoom);

  // draw once the rectangle covering the whole canvas
  // with normal transforms
  ctx.beginPath();
  ctx.rect(0, 0, canvas.width, canvas.height);

  // the checkerboard
  ctx.fillStyle = checkerboard_patt;
  // our path is already drawn, we can control only the fill
  ctx.scale(checkerboard_zoom, checkerboard_zoom);
  // avoid antialiasing when painting our pattern (similar to rounding the zoom level)
  ctx.imageSmoothingEnabled = false;
  ctx.fill();
  // done, reset to normal
  ctx.imageSmoothingEnabled = true;
  ctx.setTransform(1, 0, 0, 1, 0, 0);

  // paint the grid
  ctx.fillStyle = grid_patt;
  // because our grid is drawn in the middle of the pattern
  ctx.translate(Math.round(grid_zoom/2), Math.round(grid_zoom/2));
  ctx.fill();
  // reset
  ctx.setTransform(1, 0, 0, 1, 0, 0);
}
draw();

checkerboard_input.oninput = grid_input.oninput = function(e) {
  if(connector.checked) {
    checkerboard_input.value = grid_input.value = this.value;
  }
  draw();
};
connector.oninput = e => checkerboard_input.oninput();
<label>checkerboard-layer zoom<input id="checkerboard_input" type="range" min="2" max="50" step="0.1"></label><br>
<label>grid-layer zoom<input id="grid_input" type="range" min="2" max="50" step="1"></label><br>
<label>connect both zooms<input id="connector" type="checkbox"></label>
<canvas id="canvas"></canvas>


推荐阅读