14

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

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

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

查看以下要点,通过在控制台中执行它,您可以看到问题。

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

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

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

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

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

4

4 回答 4

24

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

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

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

您可以在此处阅读更多相关信息。

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

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

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

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

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

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

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

在此处输入图像描述

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

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

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

于 2012-04-15T23:12:16.973 回答
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

观察结果

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

分析

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

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

结论

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

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

于 2016-02-22T02:04:05.203 回答
1

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

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

于 2012-04-16T09:52:44.630 回答
0

复合模式与您找到的一样。据我所知,如果不手动设置像素,您无法合成比这更好的。如果您通过明确描述您想要如何混合像素来更新您的问题,我将相应地更新我的答案。

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

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

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

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

你的第一个选项比第二个选项更通用,因为你可以使用普通的画布方法绘制任何你喜欢的东西,然后将这个画布混合到你的可见画布上。有关如何将一个上下文混合到另一个上下文的好主意,请参阅@Phrogz上下文混合器项目

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

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

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

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

并且本着选项 2 的精神,如果您能够在渲染之前定义所有点,您可以在一次通过中从这些点计算图像和混合。

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

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


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

if (src.a <= dst.a) {
    result = dst;
} else {
    result = src;
}
于 2012-04-12T12:46:30.607 回答