Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple light sources on canvas

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:

enter image description here

This is achieved by placing a .png image above everything else that becomes more transperant towards the center, like this:

enter image description here

Works great for one light source, but I need another approach where I can add more and move the light sources around.

enter image description here

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);
like image 431
Magnus Engdal Avatar asked Nov 05 '13 10:11

Magnus Engdal


Video Answer


2 Answers

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/

let's play with candles and lights

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.

like image 161
GameAlchemist Avatar answered Oct 02 '22 04:10

GameAlchemist


  1. your png should have full transparent corners and not transparent white in middle.
  2. or you can draw this, but not pixel by pixel like here jsfiddle.net/pr9r7/2/

More examples: jsfiddle.net/pr9r7/3/ http://codepen.io/cwolves/pen/prvnb

like image 31
ViliusL Avatar answered Oct 02 '22 05:10

ViliusL