HTML5 Canvas globalCompositeOperation 用于叠加渐变而不增加强度?

Posted

技术标签:

【中文标题】HTML5 Canvas globalCompositeOperation 用于叠加渐变而不增加强度?【英文标题】:HTML5 Canvas globalCompositeOperation for overlaying gradients not adding up to higher intensity? 【发布时间】:2012-04-21 01:27:22 【问题描述】:

我目前正在修复heatmap.js,我想知道是否有人知道是否可以使用<canvas> 的二维渲染上下文来实现以下效果。

我有一个从黑色(alpha 0.5)到透明 40 像素半径的径向渐变。径向梯度的中心在 x=50, y=50 我有另一个从黑色(alpha 0.5)到透明的径向渐变,半径为 40 像素。径向梯度的中心在 x=80, y=50

两个渐变重叠。我现在的问题是:重叠区域加在一起导致 alpha 值高于径向梯度中心,从而显示错误数据(例如,由于梯度之间的添加,热图中较热的区域)

看看下面的gist,通过在你的控制台中执行它就可以看到问题所在。

预期的行为是: 最暗的区域是渐变中心,两个渐变的重叠区域合并但不相加。

在看到 globalCompositeOperations 都没有导致预期的行为后,我尝试了这些操作的组合。 我认为可能的方法如下:

绘制第一个渐变 使用复合操作'destination-out' 绘制第二个渐变 -> 从第一个渐变中减去重叠区域 使用compositeOperation 'source-over' 再次绘制第二个渐变

但不幸的是,我没有找到有效的组合。我很想听听您的反馈,在此先感谢!

PS:我知道这可以通过手动操作像素来完成,但我想知道是否有更简单、更优雅和更快的解决方案。

【问题讨论】:

两个渐变的重叠区域合并但不相加。 - 你能逐个像素地描述合并吗? 【参考方案1】:

这真的很古怪,但它可以在不涉及 imageData 的情况下做你想做的事。

想到的事情是,当您抚摸它们时,您想要路径本身在画布上具有的确切功能。引用规范:

由于跟踪路径的算法是如何定义的,因此一次笔划操作中路径的重叠部分被视为它们的并集是绘制的。

你可以阅读更多关于here.

无论如何,本质上,你想要的是一条模糊的路径,只有弧线,你可以划一次,你会完美地得到你想要的效果。

唯一的问题是无法在画布中制作模糊路径。或者几乎没有办法。

我们可以使用路径的阴影来模拟遵循路径相同规则的模糊圆圈,而不是使用路径本身。

那么,唯一的问题是你不想看到实际的路径,你只想看到它的影子。使描边透明不起作用:阴影只会绘制,不会以比其阴影对象更高的不透明度绘制。

但阴影确实具有shadowOffsetXshadowOffsetY 属性,它们通常用于将阴影移动一两个像素以产生光源的错觉。

但是,如果我们将阴影画得太远以至于您看不到它们怎么办?或者更确切地说,如果我们将路径画得太远以至于你看不到它们,你只能看到阴影怎么办?

好吧,这恰好可以解决问题。这是一个快速的结果,您的原始代码在顶部,阴影是第二个画布:

就渐变和大小而言,它与您之前所拥有的不完全一样,但它非常接近,我相信通过摆弄这些值,您可以让它更接近。几个 console.log 确认我们想要的东西,一个不超过 124(共 255 个)的 alpha 正确地出现在它曾经是 143 和 134 的地方以旧的方式进行。

查看实际代码的小提琴:http://jsfiddle.net/g54Mz/

所以你有它。如果您使用阴影并将实际路径偏移太多以至于它们不在屏幕上,则无需imageData 即可获得两个径向渐变的联合效果。

【讨论】:

感谢您的回答,这是一个非常巧妙的技术。我很快就一起破解了一个实现,它似乎工作正常。 :)【参考方案2】:

我正在开发一个基于 html5 的游戏,我想在其中混合在网格中数百个方形单元格上绘制的不同颜色的半圆形区域。效果类似于热图。经过一番研究,我发现了上面由 Simon Sarris 记录的“阴影”技术。

实施此技术可实现我想要的外观。我喜欢它很容易理解。然而,在实践中,我发现与我之前绘制数千个填充矩形的技术(但不吸引人)相比,即使渲染几个(约 150 个)阴影也要慢得多。

所以我决定做一些分析。我编写了一些基本的 javascript(其修改版本可以在 https://jsfiddle.net/Flatfingers/4vd22rgg/ 看到),将五种不同形状类型中的每一种绘制 2000 个副本到 1250x600 画布的非重叠部分,记录这五种操作中每一种的经过时间五个主要桌面浏览器和移动 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

观察

    在填充形状中,填充矩形始终是所有浏览器和测试环境中最快的操作。 在 IE 11 和 Edge 中,填充的完整圆弧(圆形)比填充的矩形慢大约 10 倍,而在其他主要浏览器中大约慢 3.5 倍。 在 IE 11、Chrome 48 和 Opera 34 中,渐变大约比矩形慢 3 倍,但在 Firefox 44 中慢了 100 倍(参见 Bugzilla report 728453)。 在所有桌面浏览器中,通过 drawImage() 生成图像的速度大约是填充矩形的 1.5 倍。 阴影填充弧是最慢的,在 IE、Edge、Chrome 和 Opera 中比填充矩形慢约 50 倍,在 Firefox 中慢 100 倍。 Chrome 48 和 Opera 34 在除阴影填充弧外的所有形状类别中都非常快速,但它们并不比那里的其他浏览器差。 当 drawImage() 绘制 1000 个形状且 shadowOffsetX 或 shadowOffsetY 的值超出物理屏幕分辨率时,Chrome 和 Opera 崩溃。 IE 11 和 Edge 绘制弧线和渐变的速度比其他桌面浏览器慢。 drawImage() 在移动 Safari 上运行缓慢。实际上,绘制多个渐变和阴影弧线比使用 drawImage() 绘制多次副本要快。 在 Firefox 中绘图对先前的操作很敏感:绘制阴影和渐变会使绘制弧线变慢。显示最快的时间。 Mobile Safari 中的绘图对先前的操作很敏感:阴影使渐变变慢;渐变和弧线使 drawImage() 甚至比通常更慢。显示最快的时间。

分析

虽然 shadowOffset 功能是一种简单且视觉上有效的混合形状的方法,但它比所有其他技术慢得多。这限制了它对只需要绘制少量阴影并且不需要快速重复绘制很多阴影的应用程序的有用性。此外,当使用 drawImage() 加速时,给 shadowOffsetX 或 shadowOffsetY 一个大于大约 3000 的值会导致 Chrome 48 和 Opera 34 挂起近一分钟,消耗 CPU 周期,然后使我的 nVidia 显示驱动程序崩溃,即使在更新它之后也是如此到最新版本。 (Google 搜索没有发现 Chromium 的错误报告描述了同时使用大 shadowOffset 和 drawImage() 时出现的此错误。)

对于需要混合模糊形状的应用程序,与阴影在视觉上最相似的方法是将 globalCompositeOperation 设置为“更轻”,并使用带有 globalAlpha 值的 drawImage() 重复绘制预先绘制的径向渐变,或者在以下情况下绘制单独的渐变它们需要是不同的颜色。这不是重叠阴影的完美匹配,但它很接近并且避免了按像素计算。 (但是,请注意,在移动 Safari 中,直接绘制阴影填充弧实际上比渐变和 drawImage() 更快。)虽然将 globalCompositeOperation 设置为“lighter”,但使用径向渐变会导致 IE 11 和 Edge 在绘制弧时慢约 10 倍仍然比在所有主要桌面浏览器中使用阴影填充弧更快,并且仅比移动 Safari 中的阴影填充弧慢两倍。

结论

如果您的唯一目标平台是 iPad/iPhone,则获得漂亮混合形状的最快方法是阴影填充弧。否则,到目前为止,我发现在所有主要桌面浏览器中都可以使用的具有可比外观的最快方法是绘制径向渐变并将 globalCompositeOperation 设置为“更轻”并使用 globalAlpha 控制不透明度。

注意:在我执行的绘图测试中,有一些明显的方法可以提高性能。特别是,将每个形状的每个实例都绘制到屏幕外缓冲区,然后将整个缓冲区绘制到可见画布上一次,这将产生显着的性能改进。但这会否定此测试的目标,即比较在可见画布上绘制不同类型形状的相对速度。

【讨论】:

【参考方案3】:

这个小提琴http://jsfiddle.net/2qQLz/ 试图提供一个解决方案。如果它接近您的需要,则可以进一步开发。它将渐变填充限制为一个边界矩形,该矩形的一侧是“圆圈”的相交线。对于位于水平线上的两个相同半径的“圆”,很容易找到“圆”交点的 x 值,并为每个“圆”绘制渐变填充的边界矩形。

两个任意的“圆”会更困难,但仍然可以找到相交线,并且仍然可以为每个“圆”绘制一个边界矩形。随着更多“圆圈”的添加,它会变得越来越复杂。

【讨论】:

【参考方案4】:

composite modes 与您找到的一样。据我所知,如果没有手动setting pixels,您无法合成比这更好的组合。如果您通过明确描述您希望如何混合像素来更新您的问题,我将相应地更新我的答案。

那么什么是逐像素解决方案?

我可以看到有两种主要的基于像素的方法可以开始解决这个问题

    绘制到隐藏的上下文并与手动功能混合

    编写一个手动计算梯度并应用自定义混合函数的函数。

您的第一个选项比第二个选项更通用,因为您可以使用普通画布方法绘制您喜欢的任何内容,然后将此画布混合到您的可见画布上。请参阅@Phrogz context-blender 项目,了解如何将一个上下文融合到另一个上下文中

当您需要以画布默认不方便的方式进行绘制时,第二个选项是必需的。

要解决的最大困难是 alpha 透明度,因为您可以在径向渐变后面有内容。在背景图像上绘制后,几乎不可能看到在其顶部绘制之前的内容,除非您保留该背景的副本。即使是在每个像素的基础上,您也会遇到困难:将一个图像混合到另一个图像的顶部是行不通的

从本质上讲,这意味着在可见画布上合成多个半透明渐变的图像,无论您选择通用合成功能如何,除非该画布一开始就具有透明背景,否则将无法正常工作。

本着选项 1 的精神,您可以创建一个空白上下文并在其上渲染多个渐变。然后在上面渲染它。

本着选项 2 的精神,如果您能够在渲染前定义所有点,则可以一次计算图像并根据这些点进行混合。

将一次性渲染技术与背景上下文相结合,您可以在可见画布顶部绘制轮廓,而无需手动读取单个像素,仅写入像素。

远不及我所知道的优雅,但它可能是在 2D 画布上实现我所说的 alpha 混合轮廓效果的唯一真正方法。


我认为您需要的每像素混合函数会从源和目标中选择具有最大 alpha 值的像素。

if (src.a <= dst.a) 
    result = dst;
 else 
    result = src;

【讨论】:

以上是关于HTML5 Canvas globalCompositeOperation 用于叠加渐变而不增加强度?的主要内容,如果未能解决你的问题,请参考以下文章

html5 canvas 元素有啥用

html5怎么在canvas圆里写字

html5 canvas大小怎么无效

html5 canvas绘图有啥用

html5 canvas怎么载入图像

html5 canvas怎么载入图像