使用浮点数时,fillRect()不能完全重叠

3
以下代码(jsFiddle)在画布上随机绘制红色正方形,并注意通过使用ctx.fillRect()填充一个白色正方形来清除先前的正方形:
<html>
  <canvas id='canvas' width=300 height=300/>
 <script>
   const ctx = document.getElementById('canvas').getContext('2d');
   let prevRect = null;
   for (let i = 0 ; i < 10; i++) {
     if (prevRect != null) {
       ctx.fillStyle='white';
       ctx.fillRect(prevRect.x, prevRect.y, 50, 50);
     }
     ctx.fillStyle='red';
     const newRect = {x: Math.random()*(300-50), y: Math.random()*(300-50)};
     ctx.fillRect(newRect.x, newRect.y, 50, 50);
     prevRect = newRect;
   }
  </script>
</html>

代码无法完全擦除先前的正方形,屏幕上仍保留着瑕疵。如果我这样做:
const newRect = {x: Math.floor(Math.random()*(300-50)), y: Math.floor(Math.random()*(300-50))};

我有一个问题,为什么要这样做。看起来完全没有必要进行截断,因为我在prevRect中保留了值,所以两个对fillRect()的调用使用完全相同的坐标(即使使用浮点数),因此这两个正方形应该始终完美对齐。

然后一切都按预期工作。

2个回答

2

如何渲染半个像素为红色?

通过基于前景色与背景色重叠的百分比进行插值,来确定背景颜色,并将其与前景色混合。当您现在尝试“删除”它时,使用background-color进行相同操作。

因此,像素的颜色可以用数学方式表示:

var afterRender = interpolate(background, forground, percentage);
var afterDelete = interpolate(afterRender, background, percentage);

让我们来算一些数字:(一个快速而简单的例子)

const update = () => {
  var bgColor = +bg.value.replace(/^#?/, "0x");
  bg.style.backgroundColor = toColor(bgColor);
  
  var fgColor = +fg.value.replace(/^#?/, "0x");
  fg.style.backgroundColor = toColor(fgColor);
  
  var percentage = overlap.value / 100;

  var afterRenderColor = interpolate(bgColor, fgColor, percentage);
  
  afterRender.textContent = afterRender.style.background = toColor(afterRenderColor);
  
  // now trying to erase this by overwriting with the background-color
  var afterDeleteColor = interpolate(afterRenderColor, bgColor, percentage);
  
  afterDelete.textContent = afterDelete.style.background = toColor(afterDeleteColor);
}

const toColor = v => "#" + v.toString(16).padStart(6, 0).toUpperCase();

const interpolate = (a, b, t) => ((a&0xFF0000) * (1-t) + (b&0xFF0000) * t) & 0xFF0000
    | ((a&0x00FF00) * (1-t) + (b&0x00FF00) * t) & 0x00FF00
    | ((a&0x0000FF) * (1-t) + (b&0x0000FF) * t) & 0x0000FF;


[bg, fg, overlap].forEach(input => input.onchange = input.oninput = update);
update();
#bg,
#fg,
#afterRender,
#afterDelete {
  border: 1px solid black;
  padding: 20px;
}
<label>Background: <input id="bg" type="text" pattern="/#?[0-9a-f]{6}/" value="#FFFFFF"/></label>
<br>
<label>Foreground: <input id="fg" type="text" pattern="/#?[0-9a-f]{6}/" value="#FF0000"/></label>
<br>
<label>"half a pixel" of overlap: <input id="overlap" type="range" min="0" max="100" value="50"></label>
more or less ;)

<br>
<br>

Color after rendering "half a pixel" of the foreground over the background:
<div id="afterRender"></div>

<br>
Color after trying to erase that by rendering "half a pixel" of the background-color over that:
<div id="afterDelete"></div>


这个回答很有希望!你能解释一下为什么第二个 interpolate 不会使用背景吗?我本来会这样写: var afterDelete = interpolate(background, newForeground, percentage); - kotchwane
因为在渲染前景后,这个像素不再具有背景颜色。如果像素仍然包含背景颜色,则意味着以下两种情况之一:1. 前景和背景在此处具有相同的颜色,或者2. 前景在此处是100%透明的。这对你有意义吗? - Thomas
那么你称“背景”是像素在改变之前的颜色,是这样吗?按照它的名字,我本以为它是一般的背景颜色。我在哪里可以获取有关JS中插值何时以及如何进行的更多信息?我仍然觉得我没有完整的图片... - kotchwane
我称呼“background”为像素在改变之前的颜色,“foreground”则是我正在绘制形状的颜色。在将FG渲染到BG上之后,我尝试通过使用BG颜色在其上方再次渲染相同的形状来擦除它。 - Thomas

0
问题源于画布上绘制图形的基本方式。当你绘制一个正方形时,形状的边缘会略微“羽化”。因此,当你在之前的红色框上绘制白色框时,红色残留物会渗入半透明边缘。
如果你绘制10个白色框而不是一个,问题就会消失。或者,如果你将白色框的大小增加0.5像素,那可能会有所帮助。

const ctx = document.getElementById('canvas').getContext('2d');
   let prevRect = null;
   for (let i = 0 ; i < 10; i++) {
     if (prevRect != null) {
       ctx.fillStyle='white';
       ctx.fillRect(prevRect.x - 0.75, prevRect.y - 0.75, 51.5, 51.5);
     }
     ctx.fillStyle='red';
     const newRect = {x: Math.random()*(300-50), y: Math.random()*(300-50)};
     ctx.fillRect(newRect.x, newRect.y, 50, 50);
     prevRect = newRect;
   }
body, html { padding: 0; }

canvas { border: 1px solid black; }
<canvas id=canvas height=300 width=300></canvas>

看起来每边增加0.75大小效果很不错,但这肯定与画布的“逻辑”尺寸与实际屏幕尺寸有关。


你能提供一个描述“feathering”的链接吗?我原以为fillRect()只是在内部截断浮点数来完成它的工作。我也不知道我在画布上绘制的矩形有一个“半透明边缘”。 - Marcus Junius Brutus
@MarcusJuniusBrutus 我会看看能否找到参考资料,但基本思想是画布的“大小”是概念上的大小。浏览器将其映射到实际屏幕像素大小,因此无论是浮点数还是其他类型,都很可能存在一些模糊边缘。 - Pointy
1
@MarcusJuniusBrutus 请注意,当您从A到B绘制一条线时,描边宽度位于线的两侧。如果描边宽度为1,则该线在两个点之间的几何(零宽度)线上向两侧扩展0.5像素。在画布上绘图并不像人们最初想象的那样简单。 - Pointy
我能理解为什么对角线会出现问题,但我认为对于矩形和水平/垂直线条,一切都应该很清晰。但是你说得对,画布的概念尺寸必须映射到实际像素上。因此,似乎没有办法精确地“擦除”(通过与白色重叠)以前绘制的形状,除非清除并重新绘制整个画布。这使得单个形状在画布上移动的动画(我们知道它不会覆盖其他形状)变得不必要地昂贵,因为我们必须重新绘制所有内容。 - Marcus Junius Brutus
@MarcusJuniusBrutus,画布功能的许多细节都让我感到头疼,但我们能做什么呢? - Pointy
然而,在现代浏览器(和现代电脑)上,@MarcusJuniusBrutus的这个设施非常快。 - Pointy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接