Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

HTML5 Canvas globalCompositeOperation for overlaying gradients not adding up to higher intensity?

I'm currently working on a heatmap.js fix and I was wondering whether anyone knows if it's possible to achive the following effect with <canvas>'s 2d rendering context.

  • I have a radial gradient from black (alpha 0.5) to transparent 40pixel radius. center of the radial gradient is at x=50, y=50
  • I have another radial gradient from black (alpha 0.5) to transparent, 40pixel radius. center of the radial gradient is at x=80, y=50

The two gradients are overlapping. My problem now is: the overlapping area gets added up together resulting in a higher alpha value than the radial gradients centers and thus showing wrong data (e.g. hotter areas in a heatmap because of those additions between the gradients)

Have a look at the following gist, by executing it in your console you can see the problem.

Expected behaviour would be: Darkest areas are the gradients centers, the overlapping area of the two gradients merges but doesn't add up.

After seeing that none of the globalCompositeOperations resulted in the expected behaviour I tried combinations of those operations. A way I thought it maybe would be possible was the following:

  • draw first gradient
  • use compositeOperation 'destination-out'
  • draw second gradient -> substracts overlapping area from the first gradient
  • use compositeOperation 'source-over'
  • draw second gradient again

But unfortunately I didn't find a combination that worked. I'd love to hear your feedback, thanks in advance!

PS: I know this could be done by manipulating the pixels manually, but I was wondering whether there's an easier, more elegant and faster solution for that.

like image 832
Patrick Wied Avatar asked Apr 08 '12 03:04

Patrick Wied


3 Answers

This is really wacky but it does what you want without getting imageData involved.

The thing that came to mind was that you want the exact functionality that paths themselves have on the canvas when you stroke them. To quote the spec:

As a result of how the algorithm to trace a path is defined, overlapping parts of the paths in one stroke operation are treated as if their union was what was painted.

You can read more about that here.

Anyway, what you'd want, essentially, is a blurry path of nothing but arcs that you can stroke once and you'd perfectly get the effect you were looking for.

The only problem is that there is no way to make a blurry path in canvas. Or almost no way.

Instead of using the path itself we can use the shadow of a path in order to simulate blurry circles that obey the same rules that paths do.

The only problem there, then, is that you don't want to see the actual path, you just want to see the shadow of it. Making the stroke transparent won't work: A shadow will only draw will not draw at a higher opacity than the thing it is shadowing.

But shadows do have the properties shadowOffsetX and shadowOffsetY, which are typically used to shift the shadow by a pixel or two to make the illusion of a light source.

But what if we draw the shadows so far away that you can't see them? Or rather, what if we draw the paths so far away that you can't see them, all you can see are the shadows?

Well that happens to do the trick. Here is a quick result, your original code is on the top and the shadows are the second canvas:

enter image description here

It's not exactly what you had before in terms of gradients and size but its very close and I'm sure that by fiddling with the values you can get it even closer. A couple of console.log's confirm that the thing we want, an alpha that does not go above 124 (out of 255) is correctly occurring in the places where it used to be 143 and 134 doing it the old way.

The fiddle to see the code in action: http://jsfiddle.net/g54Mz/

So there you have it. Getting the effect of the union of two radial gradients is possible without imageData if you use shadows and offset the actual paths so much that they are off the screen.

like image 138
Simon Sarris Avatar answered Sep 28 '22 00:09

Simon Sarris


I'm working on an HTML5-based game in which I want to blend differently-colored semi-circular areas drawn over hundreds of square cells in a grid. The effect is something like a heat map. After some research, I discovered the "shadows" technique documented above by Simon Sarris.

Implementing this technique delivered the look I wanted. And I liked that it was easy to understand. However, in practice I found that rendering even a few (~150) shadows was much slower compared to my previous technique (however unattractive) of drawing thousands of filled rects.

So I decided to do some analysis. I wrote some basic JavaScript (a modified version of which can be seen at https://jsfiddle.net/Flatfingers/4vd22rgg/ ) to draw 2000 copies each of five different shape types onto non-overlapping sections of a 1250x600 canvas, recording the elapsed time for each of these five operations in the latest versions of five major desktop browsers plus mobile Safari. (Sorry, desktop Safari. I also don't have an Android handy to test.) Then I tried different combinations of effects and recorded the elapsed times.

Here is a simplified example of how I'm drawing two gradients with an appearance similar to shadowed filled arcs:

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();

TIMINGS

(2000 non-overlapping shapes, sets globalAlpha, drawImage() is used for gradients but not shadows)

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

OBSERVATIONS

  1. Among filled shapes, filled rects are consistently the fastest operation in all browsers and environments tested.
  2. Filled full arcs (circles) are about 10x slower in IE 11 and Edge than filled rects, compared to about 3.5x slower in the other major browsers.
  3. Gradients are roughly 3x slower than rects in IE 11, Chrome 48, and Opera 34, but a remarkable 100x slower in Firefox 44 (see Bugzilla report 728453).
  4. Images via drawImage() are roughly 1.5x as fast as filled rects in all desktop browsers.
  5. Shadowed filled arcs are slowest of all, ranging from around 50x slower than filled rects in IE, Edge, Chrome and Opera to 100x slower in Firefox.
  6. Chrome 48 and Opera 34 are both remarkably speedy in every category of shape except shadowed filled arcs, but they're no worse than other browsers there.
  7. Chrome and Opera crash when drawImage() draws 1000 shapes where either shadowOffsetX or shadowOffsetY is given a value outside the physical screen resolution.
  8. IE 11 and Edge are slower to paint arcs and gradients than other desktop browsers.
  9. drawImage() is slow on mobile Safari. It's actually faster to draw multiple gradients and shadowed arcs than to draw one copy many times with drawImage().
  10. Drawing in Firefox is sensitive to prior operations: drawing shadows and gradients makes drawing arcs slower. Fastest times are shown.
  11. Drawing in Mobile Safari is sensitive to prior operations: shadows make gradients slower; gradients and arcs make drawImage() even slower than it normally is. Fastest times are shown.

ANALYSIS

While the shadowOffset feature is a simple and visually effective way to blend shapes, it is significantly slower than all other techniques. This limits its usefulness to applications that only need to draw a few shadows, and that do not need to draw many shadows quickly and repeatedly. Furthermore, when sped up using drawImage(), giving either shadowOffsetX or shadowOffsetY a value larger than about 3000 causes Chrome 48 and Opera 34 to hang for nearly a minute, consuming CPU cycles, and then crashes my nVidia display driver, even after updating it to the latest version. (Google Search found no bug reports for Chromium describing this error when a large shadowOffset and drawImage() are used together.)

For applications that need to blend indistinct shapes, the most visually similar approach to shadows is to set globalCompositeOperation to "lighter" and use drawImage() with a globalAlpha value to repeatedly draw a prepainted radial gradient, or to draw individual gradients if they need to be different colors. This is not a perfect match for overlapping shadows, but it's close and avoids doing per-pixel calculations. (However, note that in mobile Safari, directly drawing shadowed filled arcs actually is faster than gradients and drawImage().) While setting globalCompositeOperation to "lighter" causes IE 11 and Edge to be about 10x slower in drawing arcs, using a radial gradient is still faster than using shadowed filled arcs in all of the major desktop browsers, and only twice as slow than shadowed filled arcs in mobile Safari.

CONCLUSION

If your only target platform is iPad/iPhone, the fastest method for nice-looking blended shapes is shadowed filled arcs. Otherwise, the fastest method with comparable appearance that I have found so far that works in all of the major desktop browsers is drawing radial gradients with globalCompositeOperation set to "lighter" and controlling opacity with globalAlpha.

Note: There are some obvious ways that performance could be improved in the drawing tests I performed. In particular, drawing every instance of each shape to an offscreen buffer and then drawing that whole buffer one time onto the visible canvas would yield a significant performance improvement. But that would have negated the goal of this testing, which was to compare the relative speeds of drawing the different kinds of shapes on the visible canvas.

like image 36
Bart Stewart Avatar answered Sep 28 '22 00:09

Bart Stewart


This fiddle http://jsfiddle.net/2qQLz/ is an attempt to provide a solution. If it is close to what you need it could be developed further. It limits the gradient fill to a bounding rectangle one side of which is the line of intersection of the 'circles'. For two 'circles' of the same radius lying along a horizontal line it is easy enough to find the x value of the points of intersection of the 'circles' and to draw bounding rectangles for the gradient fill for each 'circle'.

It would be more difficult for two arbitrary 'circles' but the line of intersection could still be found and a bounding rectangle could still be draw for each 'circle'. It would become progressively more complicated as more 'circles' were added.

like image 27
jing3142 Avatar answered Sep 28 '22 01:09

jing3142