[英]HTML5 Canvas globalCompositeOperation for overlaying gradients not adding up to higher intensity?
我目前正在修复heatmap.js ,我想知道是否有人知道是否可以使用<canvas>
的 2d 渲染上下文实现以下效果。
两个梯度重叠。 我现在的问题是:重叠区域被加在一起导致比径向渐变中心更高的 alpha 值,从而显示错误的数据(例如,由于渐变之间的这些添加,热图中更热的区域)
看看下面的要点,通过在控制台中执行它,您可以看到问题所在。
预期的行为是:最暗的区域是渐变中心,两个渐变的重叠区域合并但不相加。
在看到没有任何 globalCompositeOperations 导致预期的行为后,我尝试了这些操作的组合。 我认为可能的一种方式如下:
但不幸的是,我没有找到有效的组合。 我很想听听您的反馈,在此先感谢您!
PS:我知道这可以通过手动操作像素来完成,但我想知道是否有更简单、更优雅、更快速的解决方案。
这真的很古怪,但它可以在不涉及 imageData 的情况下做你想做的事。
我想到的是,当您抚摸它们时,您想要路径本身在 canvas 上具有的确切功能。 引用规范:
由于跟踪路径的算法是如何定义的,一次笔画操作中路径的重叠部分被视为它们的并集就是所绘制的。
您可以在此处阅读更多相关信息。
无论如何,从本质上讲,您想要的是一条模糊的路径,只有弧线,您可以划一次,就可以完美地获得您想要的效果。
唯一的问题是canvas中没有办法弄模糊路径。或者说几乎没有办法。
除了使用路径本身,我们还可以使用路径的阴影来模拟遵循与路径相同规则的模糊圆圈。
那么,唯一的问题是您不想看到实际路径,而只想看到它的影子。 使描边透明是行不通的:只会绘制的阴影不会以比其阴影对象更高的不透明度进行绘制。
但阴影确实具有属性shadowOffsetX
和shadowOffsetY
,它们通常用于将阴影移动一两个像素以产生光源的错觉。
但是,如果我们将阴影画得很远以至于您看不到怎么办? 或者更确切地说,如果我们把路径画得很远以至于你看不到它们,你只能看到阴影怎么办?
好吧,这恰好可以解决问题。 这是一个快速结果,您的原始代码在顶部,阴影是第二个 canvas:
就渐变和大小而言,这与您之前所拥有的并不完全相同,但它非常接近,我相信通过摆弄这些值,您可以使它更接近。 几个 console.log 确认我们想要的东西,一个 go 不高于 124(255 个)的 alpha 正确地出现在它曾经是 143 和 134 的地方,以旧的方式进行。
查看实际代码的小提琴: http://jsfiddle.net/g54Mz/
所以你有它。 如果您使用阴影并将实际路径偏移到屏幕之外,则无需imageData
即可获得两个径向渐变的联合效果。
我正在开发一款基于 HTML5 的游戏,我想在其中混合绘制在网格中数百个方形单元格上的不同颜色的半圆形区域。 效果类似于热 map。经过一些研究,我发现了上面由 Simon Sarris 记录的“阴影”技术。
实施此技术可实现我想要的外观。 我喜欢它很容易理解。 然而,在实践中,我发现与我以前绘制数千个填充矩形的技术(尽管没有吸引力)相比,即使渲染几个(~150)个阴影也要慢得多。
所以我决定做一些分析。 我写了一些基本的 JavaScript(其修改版本可以在https://jsfiddle.net/Flatfingers/4vd22rgg/上看到)在 1250x600 canvas 的非重叠部分上绘制 2000 个五种不同形状类型的副本,记录五种主要桌面浏览器的最新版本加上移动端Safari中这五种操作的运行时间。(抱歉,桌面Safari。我也没有Android方便测试。)然后我尝试了不同的效果组合并记录了经过的时间。
这是我如何绘制两个渐变的简化示例,其外观类似于阴影填充弧:
var gradient1 = context.createRadialGradient(75,100,2,75,100,80);
gradient1.addColorStop(0,"yellow");
gradient1.addColorStop(1,"black");
var gradient2 = context.createRadialGradient(125,100,2,125,100,80);
gradient2.addColorStop(0,"blue");
gradient2.addColorStop(1,"black");
context.beginPath();
context.globalCompositeOperation = "lighter";
context.globalAlpha = 0.5;
context.fillStyle = gradient1;
context.fillRect(0,0,200,200);
context.fillStyle = gradient2;
context.fillRect(0,0,200,200);
context.globalAlpha = 1.0;
context.closePath();
时序
(2000 个非重叠形状,设置 globalAlpha,drawImage() 用于渐变但不用于阴影)
IE 11 (64-bit Windows 10)
Rects = 4 ms
Arcs = 35 ms
Gradients = 57 ms
Images = 8 ms
Shadows = 160 ms
Edge (64-bit Windows 10)
Rects = 3 ms
Arcs = 47 ms
Gradients = 52 ms
Images = 7 ms
Shadows = 171 ms
Chrome 48 (64-bit Windows 10)
Rects = 4 ms
Arcs = 10 ms
Gradients = 8 ms
Images = 8 ms
Shadows = 203 ms
Firefox 44 (64-bit Windows 10)
Rects = 4 ms
Arcs = 21 ms
Gradients = 7 ms
Images = 8 ms
Shadows = 468 ms
Opera 34 (64-bit Windows 10)
Rects = 4 ms
Arcs = 9 ms
Gradients = 8 ms
Images = 8 ms
Shadows = 202 ms
Mobile Safari (iPhone5, iOS 9)
Rects = 12 ms
Arcs = 31 ms
Gradients = 67 ms
Images = 82 ms
Shadows = 32 ms
观察
分析
虽然 shadowOffset 功能是混合形状的一种简单且视觉上有效的方法,但它比所有其他技术慢得多。 这将它的用处限制在只需要绘制少量阴影的应用程序中,而不需要快速重复地绘制许多阴影。 此外,当使用 drawImage() 加速时,给 shadowOffsetX 或 shadowOffsetY 一个大于 3000 的值会导致 Chrome 48 和 Opera 34 挂起近一分钟,消耗 CPU 周期,然后崩溃我的 nVidia 显示驱动程序,即使在更新它之后到最新版本。 (谷歌搜索没有发现 Chromium 的错误报告描述了当一个大的 shadowOffset 和 drawImage() 一起使用时的这个错误。)
对于需要混合模糊形状的应用程序,视觉上最相似的阴影方法是将 globalCompositeOperation 设置为“更亮”,并使用具有 globalAlpha 值的 drawImage() 来重复绘制预绘制的径向渐变,或者在需要时绘制单独的渐变不同 colors。这不是重叠阴影的完美匹配,但它很接近并且避免了逐像素计算。 (但是,请注意,在移动设备 Safari 中,直接绘制阴影填充圆弧实际上比渐变和 drawImage() 更快。)将 globalCompositeOperation 设置为“更轻”会导致 IE 11 和 Edge 在使用径向渐变绘制圆弧时慢大约 10 倍仍然比在所有主要桌面浏览器中使用阴影填充弧快,并且仅比移动 Safari 中的阴影填充弧慢两倍。
结论
如果您的唯一目标平台是 iPad/iPhone,那么获得漂亮混合形状的最快方法是阴影填充弧。 否则,到目前为止,我发现在所有主要桌面浏览器中工作的具有可比外观的最快方法是绘制径向渐变,将 globalCompositeOperation 设置为“更亮”,并使用 globalAlpha 控制不透明度。
注意:在我执行的绘图测试中,有一些明显的方法可以提高性能。 特别是,将每个形状的每个实例绘制到屏幕外缓冲区,然后将整个缓冲区绘制一次到可见的 canvas 将产生显着的性能改进。 但这会否定此测试的目标,即比较在可见 canvas 上绘制不同形状的相对速度。
这个小提琴http://jsfiddle.net/2qQLz/试图提供一个解决方案。 如果它接近您的需要,则可以进一步开发。 它将渐变填充限制为边界矩形,边界矩形的一侧是“圆圈”的交线。 对于位于水平线上的两个半径相同的“圆圈”,很容易找到“圆圈”交点的 x 值,并为每个“圆圈”绘制渐变填充的边界矩形。
对于两个任意的“圆圈”来说会更困难,但仍然可以找到相交线并且仍然可以为每个“圆圈”绘制一个边界矩形。 随着更多“圈子”的加入,它会变得越来越复杂。
复合模式如您所见。 据我所知,如果不手动设置 pixels ,你不能合成比这更好的。 如果您使用对如何混合像素的明确描述来更新您的问题,我将相应地更新我的答案。
那么什么是逐像素解决方案?
我可以看到两种主要的基于像素的方法可以开始解决这个问题
绘制隐藏的上下文并与手册混合 function
编写一个 function 手动计算梯度并应用自定义混合 function。
你的第一个选项比第二个更通用,因为你可以使用普通的 canvas 方法绘制任何你喜欢的东西,然后将这个 canvas 混合到你可见的 canvas 上。有关如何混合的好主意,请参阅@Phrogz context-blender项目一个上下文到另一个上下文
当您需要以 canvas 默认不支持的方式进行绘制时,第二个选项是必需的。
最大的困难是 alpha 透明度,因为您可能在径向渐变后面有内容。 一旦您在背景图像之上绘制,就几乎不可能看到您在其之上绘制之前的内容,除非您保留该背景的副本。 即使在每个像素的基础上,您也会遇到困难:将一个图像混合在另一个图像之上是行不通的。
从本质上讲,这意味着在可见的 canvas 之上合成多个半透明渐变的图像,无论您选择通用合成 function,除非 canvas 具有透明背景开始,否则不会起作用。
本着选项 1 的精神,您可以创建一个空白上下文并在其上呈现多个渐变。 然后在上面渲染它。
本着选项 2 的精神,如果您能够在渲染之前定义所有的点,则您可以计算图像并通过这些点在一次传递中进行混合。
将一次性渲染技术与背景上下文相结合,您可以在可见的 canvas 顶部绘制轮廓,而无需进行单个手动像素读取,仅进行像素写入。
远没有我所知道的那么优雅,但它可能是在 2D canvas 上实现我称之为 alpha 混合轮廓效果的唯一真实方法。
我认为您需要的每像素混合 function 会从源和目标中选择具有最大 alpha 值的像素。
if (src.a <= dst.a) {
result = dst;
} else {
result = src;
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.