HTML canvas:为什么大的阴影模糊不会出现在小物体上?

HTML canvas: Why does a large shadow blur not show up for small objects?

这是一个演示:

var ctx = document.getElementById("test").getContext("2d");

ctx.shadowColor = "black";
ctx.fillStyle = "white";

ctx.shadowBlur = 10;
ctx.fillRect(10, 10, 10, 10);

ctx.shadowBlur = 50;
ctx.fillRect(70, 10, 10, 10);
ctx.fillRect(70, 70, 70, 70);
<canvas id="test" width="200" height="200"></canvas>

如果我设置 shadowBlur=10 然后绘制一个 10x10 的小正方形,我会得到一个漂亮、强烈的阴影。如果我设置 shadowBlur=50 并绘制一个 70x70 的大正方形,则相同。但是如果我设置 shadowBlur=50 然后画一个 10x10 的小正方形,我会得到一个非常微弱、几乎看不见的阴影。

相反,我会期望一个小的中心正方形和周围有一个大的暗影。

显然我误解了阴影模糊的工作原理,所以 - 它是如何工作的,我如何在小物体周围获得大的暗阴影?

shadowBlur 使用高斯模糊在内部产生阴影。对象被绘制到一个单独的位图作为阴影颜色的模板,然后使用半径模糊。此步骤后它不使用原始形状。结果被合成回来(作为旁注:之前对于如何合成阴影存在分歧,因此 Firefox 和 Chrome/Opera 以不同的方式呈现它们 - 我认为他们现在已经在两个阵营中登陆 source-over虽然)。

如果对象非常小并且模糊半径非常大,则平均将被对象周围剩余的空白 space 稀释,留下更微弱的阴影。

使用内置方法获得更明显阴影的唯一方法是使用更小的半径。您还可以 "cheat" 使用径向渐变,或者绘制一个更大的对象并将阴影应用到屏幕外 canvas 但相对于阴影本身进行偏移以使对象不与它重叠,然后绘制仅阴影(使用 drawImage() 的剪切参数)在绘制主要对象之前以所需的大小返回主要 canvas。

在较新版本的浏览器中,您还可以使用新的 filter 属性 在上下文中使用 CSS 过滤器手动生成高斯模糊阴影。它确实需要一些额外的合成步骤,并且很可能在大多数情况下需要一个离屏 canvas,但是您可以使用此方法在多个步骤中透支阴影,可变半径从小到大产生更明显的阴影,代价是一些表现。

使用filter手动生成阴影的示例:

这允许使用内置阴影等更复杂的形状,但可以更好地控制最终结果。 "Falloff" 在这种情况下,可以通过在循环内使用具有初始归一化半径值的缓动函数来控制。

// note: requires filter support on context
var ctx = c.getContext("2d");

var iterations = 16, radius = 50,
    step = radius / iterations;
for(var i = 1; i < iterations; i++) {
  ctx.filter = "blur(" + (step * i) + "px)";
  ctx.fillRect(100, 50, 10, 10);
}

ctx.filter = "none";
ctx.fillStyle = "#fff";
ctx.fillRect(100, 50, 10, 10);
<canvas id=c></canvas>

gradient+filter的例子:

这是一个更跨浏览器友好的解决方案,就像不支持过滤器一样,至少渐变接近可接受的阴影。唯一的缺点是它在复杂形状方面更受限制。

此外,使用渐变的可变中心点可以模拟衰减、灯光大小、灯光类型等。

基于@Kaiido 的 example/mod 评论 -

// note: requires filter support on context
var ctx = c.getContext("2d");

var grad = ctx.createRadialGradient(105,55,50,105,55,0);
grad.addColorStop(0,"transparent");
grad.addColorStop(0.33,"rgba(0,0,0,0.5)"); // extra point to control "fall-off"
grad.addColorStop(1,"black");

ctx.fillStyle = grad;
ctx.filter = "blur(10px)";
ctx.fillRect(0, 0, 300, 150);

ctx.filter = "none";
ctx.fillStyle = "#fff";
ctx.fillRect(100, 50, 10, 10);
<canvas id=c></canvas>