Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Canvas - Fill a rectangle in all areas that are fully transparent

I'm writing a simple 2D game engine using the HTML5 canvas. I've come to adding a lighting engine. Each light source has a radius value and an intensity value (0-1, eg 1 would be very bright). There's also an ambient light value that is used to light everything else in the world that isn't near a light source (0-1, eg 0.1 would be moonlight). The process of lighting is done on a separate canvas above the main canvas:

  1. For each light source, a radial gradient is drawn at that position with the same radius as the light source. The gradient is given two stops: the center is black with an alpha of 1-intensity of the light and the end/edge is black with alpha of 1-ambient light value. That all works fine.
  2. This is where it goes wrong :/ I need to fill the whole canvas with black with and alpha of 1-ambient light value and at the moment I do this by setting the context.globalCompositeOperation to source-out then fillRecting the whole canvas.

My code for this stuff is:

var amb = 'rgba(0,0,0,' + (1-f.ambientLight) + ')';
for(i in f.entities) {
    var e = f.entities[i], p = f.toScreenPoint(e.position.x, e.position.y), radius = e.light.radius;

    if(radius > 0) {
        var g = cxLighting.createRadialGradient(p.x, p.y, 0, p.x, p.y, radius);
        g.addColorStop(0, 'rgba(0,0,0,' + (1-e.light.intensity) + ')');
        g.addColorStop(1, amb);

        cxLighting.fillStyle = g;
        cxLighting.beginPath();
        cxLighting.arc(p.x, p.y, radius, 0, Math.PI*2, true);
        cxLighting.closePath();
        cxLighting.fill();
    }
}
//Ambient light
cxLighting.globalCompositeOperation = 'source-out';
cxLighting.fillStyle = amb;
cxLighting.fillRect(0, 0, f.width, f.height);
cxLighting.globalCompositeOperation = 'source-over';

However instead of getting what I wan't out of the engine (left) I get a kind of reversed gradient (right). I think this is because when I draw the rectangle with the source-out composite operation it affects the colours of the gradient itself because they are semi-transparent.

Desired lighting effectI get this

Is there a way to do this differently or better? Using clipping maybe, or drawing the rect over everything first?

Also, I modified the Mozila Dev Centre's example on composting to replicate what I need to do and none of the composite modes seemed to work, check that out if it would help.

Thanks very much, any answer would be great :)

like image 747
jt78 Avatar asked Oct 26 '11 22:10

jt78


People also ask

How do you fill a rectangle in canvas?

The CanvasRenderingContext2D. fillRect() method of the Canvas 2D API draws a rectangle that is filled according to the current fillStyle . This method draws directly to the canvas without modifying the current path, so any subsequent fill() or stroke() calls will have no effect on it.

How do you fill a canvas shape?

To fill an HTML5 Canvas shape with a solid color, we can set the fillStyle property to a color string such as blue, a hex value such as #0000FF, or an RGB value such as rgb(0,0,255), and then we can use the fill() method to fill the shape.

How do I change transparency in canvas?

You can change the opacity of new drawings by setting the globalAlpha to a value between 0.00 (fully transparent) and 1.00 (fully opaque). The default globalAlpha is 1.00 (fully opaque). Existing drawings are not affected by globalAlpha .

Which method is used to draw a rectangle on a canvas?

To draw the rectangle onto a canvas, you can use the fill() or stroke() methods. Note: To both create and render a rectangle in one step, use the fillRect() or strokeRect() methods.


1 Answers

One trivial way would be to use imageData but that would be painfully slow. It's an option, but not a good one for a game engine.

Another way would be to think of the ambient light and the light-source as if they were one path. That would make it very easy to do:

http://jsfiddle.net/HADky/

Or see it with an image behind: http://jsfiddle.net/HADky/10/

The thing you're taking advantage of here is the fact that any intersection of a path on canvas is always only unioned and never compounded. So you're using a single gradient brush to draw the whole thing.

But it gets a bit trickier than that if there's more than one light-source. I'm not too sure how to cover that in an efficient way, especially if you plan for two light-sources to intersect.

What you should probably do instead is devise an alpha channel instead of this overlay thing, but I can't currently think of a good way to get it to work. I'll revisit this if I think of anything else.


EDIT: Hey! So I've done a bit of thinking and came up with a good solution.

What you need to do is draw a sort of alpha channel, where the dark spots mark the places where you want light to be. So if you had three light sources it would look like this:

enter image description here

Then you want to set the fill style to your ambient color and set the globalCompositeOperation to xor and xor the whole thing.

ctx.fillStyle = amb;
ctx.globalCompositeOperation = 'xor';
ctx.fillRect(0,0,500,500);

That will leave you with the "opposite" image except the transparent parts will be correctly ambient:

enter image description here

Here's a working example of the code:

http://jsfiddle.net/a2Age/

Extra optimization: You can actually achieve the effect without using any paths at all, by simply drawing the exact same radial gradients onto rects instead of circular paths:

http://jsfiddle.net/a2Age/2/ Hope that helps!

like image 159
Simon Sarris Avatar answered Oct 04 '22 18:10

Simon Sarris