javascript - 使用 drawImage 缩放上面的画布
问题描述
我正在尝试使用 2 个画布制作像素编辑器。第一个画布显示包含像素的第二个画布。第一个画布使用drawImage来定位和缩放第二个画布。
当第二个画布被缩放到小于它的原始大小时,它开始出现故障。
这是以原始尺寸显示的画布。当我放大时,第二个画布变大了,一切都很完美。
但是,当我缩小时,网格和背景(透明度)的行为非常奇怪。
要在第一个画布上绘制第二个画布,我使用函数
ctx.drawImage(drawCanvas, offset.x, offset.y, width * pixelSize, height * pixelSize);
我已经读过多次迭代中的缩放可能会提供更好的图像质量,但我不确定画布。
当用户缩小时,我可以以较低的分辨率完全重绘第二个画布,但这对 cpu 来说有点重。
有没有我不知道的更好的解决方案?
解决方案
您的问题来自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>
推荐阅读
- reactive-programming - 如何下载文件并立即流式传输 - 上传 - 使用 Spring Reactive WebClient
- linux - 作为 bindkey 函数的一部分,在 zsh 中手动调用 compinit
- python - Python lambda 函数改变它们的值
- excel - 格式化图表上的数据标签
- javascript - 如何禁用模板添加的按钮数组?
- c++ - C++ 的 spdlog;what():无法打开文件 ~/logs/log.txt 进行写入:没有这样的文件或目录
- function - flatMapMerge 缓存是否流动?
- c++ - 在 cmake 中使用 MinGW 构建 Assimp 时出现“未找到 LIB”错误
- javascript - Express,请求正文在中间件中是 {},但在控制器中有内容
- outlook-redemption - 应用程序中嵌入的赎回问题