I want to place a number of light sources on a background for a game I'm making, which works great with one light source as shown below:
This is achieved by placing a .png image above everything else that becomes more transperant towards the center, like this:
Works great for one light source, but I need another approach where I can add more and move the light sources around.
I have considered drawing a similar "shadow layer" pixel by pixel for each frame, and calculate the transparency depending of the distance to each light source. However, that would probably be very slow and I'm sure there are way better solutions to this problem.
The images are just examples and each frame will have considerably more content to move around and update using requestAnimationFrame.
Is there a light weight and simple way to achieve this? Thanks in advance!
Edit
With the help of ViliusL, I came up with this masking solution:
http://jsfiddle.net/CuC5w/1/
// Create canvas
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
canvas.width = 300;
canvas.height = 300;
document.body.appendChild(canvas);
// Draw background
var img=document.getElementById("cat");
ctx.drawImage(img,0,0);
// Create shadow canvas
var shadowCanvas = document.createElement('canvas');
var shadowCtx = shadowCanvas.getContext('2d');
shadowCanvas.width = canvas.width;
shadowCanvas.height = canvas.height;
document.body.appendChild(shadowCanvas);
// Make it black
shadowCtx.fillStyle= '#000';
shadowCtx.fillRect(0,0,canvas.width,canvas.height);
// Turn canvas into mask
shadowCtx.globalCompositeOperation = "destination-out";
// RadialGradient as light source #1
gradient = shadowCtx.createRadialGradient(80, 150, 0, 80, 150, 50);
gradient.addColorStop(0, "rgba(255, 255, 255, 1.0)");
gradient.addColorStop(1, "rgba(255, 255, 255, .1)");
shadowCtx.fillStyle = gradient;
shadowCtx.fillRect(0, 0, canvas.width, canvas.height);
// RadialGradient as light source #2
gradient = shadowCtx.createRadialGradient(220, 150, 0, 220, 150, 50);
gradient.addColorStop(0, "rgba(255, 255, 255, 1.0)");
gradient.addColorStop(1, "rgba(255, 255, 255, .1)");
shadowCtx.fillStyle = gradient;
shadowCtx.fillRect(0, 0, canvas.width, canvas.height);
Another way to play with light is to use the globalCompositeOperation mode 'ligther' to ligthen things, and just use globalAlpha to darken things.
First here's an image, with a cartoon lightening on the left, and a more realistic lightening on the right, but you'd rather watch the fiddle, since it's animated :
http://jsfiddle.net/gamealchemist/ABfVj/
So how i did things :
To darken :
- Choose a darkening color( most likely black, but you can choose a red or another color to teint the result).
- choose an opacity ( 0.3 seems a good start value ).
- fillRect the area you want to darken.
function darken(x, y, w, h, darkenColor, amount) {
ctx.fillStyle = darkenColor;
ctx.globalAlpha = amount;
ctx.fillRect(x, y, w, h);
ctx.globalAlpha = 1;
}
To lighten :
- Choose a lightening color. Beware that this color's r,g,b will be added to the previous point's r,g,b : if you use a high value your color will get burnt.
- change the globalCompositeOperation to 'lighter'
- you might change opacity also, to have more control over the lightening.
- fillRect or arc the area you want to lighten.
If you draw several circles while in lighter mode, the results will add up, so you can choose a quite low value and draw several circles.
function ligthen(x, y, radius, color) {
ctx.save();
var rnd = 0.03 * Math.sin(1.1 * Date.now() / 1000);
radius = radius * (1 + rnd);
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = '#0B0B00';
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * π);
ctx.fill();
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, radius * 0.90+rnd, 0, 2 * π);
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, radius * 0.4+rnd, 0, 2 * π);
ctx.fill();
ctx.restore();
}
Notice that i added a sinusoidal variation to make the light more living.
Ligthen : another way :
You can also, while still using the 'ligther' mode, use a gradient to have a smoother effect (first one is more cartoon like, unless you draw a lot of circles.).
function ligthenGradient(x, y, radius) {
ctx.save();
ctx.globalCompositeOperation = 'lighter';
var rnd = 0.05 * Math.sin(1.1 * Date.now() / 1000);
radius = radius * (1 + rnd);
var radialGradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
radialGradient.addColorStop(0.0, '#BB9');
radialGradient.addColorStop(0.2 + rnd, '#AA8');
radialGradient.addColorStop(0.7 + rnd, '#330');
radialGradient.addColorStop(0.90, '#110');
radialGradient.addColorStop(1, '#000');
ctx.fillStyle = radialGradient;
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * π);
ctx.fill();
ctx.restore();
}
i also added here a sin variation.
Rq : creating a gradient on each draw will create garbage : store the gradient if you use a single gradient, and store them in an array if you want to animate the gradients.
If you are using the same light in several places, have a single gradient built, centered on (0,0), and translate the canvas before drawing always with this single gradient.
Rq 2 : you can use clipping to prevent some parts of the screen to be lightened (if there's an obstacle). I added the blue circle on my example to show this.
So you might want to ligthen directly your scene with those effects, or create separately a light layer that you darken/lighten as you want before drawImage it on the screen.
There are too many scenari to discuss them here (light animated or not, clipping or not, pre-compute a light layer or not, ...) but as far as speed is concerned, for Safari and iOS safari, the solution using rect/arc draws -either with gradient or a solid fill- will be rocket faster than drawing an image/canvas.
On Chrome it will be quite the opposite : it's faster to draw an image than to draw each geometry when the geometry count raises.
Firefox is rather similar to Chrome for this.
More examples: jsfiddle.net/pr9r7/3/ http://codepen.io/cwolves/pen/prvnb
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With