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:
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.
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.
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 :)
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:
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:
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!
Just an idea, but since you're getting the opposite effect you're going for from your gradient, have you tried reversing the gradient?
Related
I want to draw a growing trail behind an moving object. I know this look quite easy :) But there are some constraints :
the trail must have some homogeneous transparency
and i can not use caching methods because of performance issues
I have tested 2 ways :
One with lineTo() and incrementing stroke width, but the alpha transparency is not homogenic...
https://jsfiddle.net/zOgs/9ntajsa1/
One with lineTo() and circles to fill the blank, transparency is OK but there is a strange behavior when drawing from left to right, negative space appears...
https://jsfiddle.net/zOgs/psa3x9y2/
I also try to use compositeOperation with something like this, but it's messing with my background...
trail.alpha = 0.5;
trail.compositeOperation = 'xor';
for(var i=nb; i>=0; i--) {
trail.graphics.drawCircle(points[i].x,points[i].y,size/2).closePath();
}
I can't find a valid solution to this problem and i am beginning to despair :(
There is probably a better way to do this, but here is an easy way: Use an off-screen canvas to draw the trails, then display that canvas as a bitmap child of the main stage.
Here is a fiddle based on your first one:
https://jsfiddle.net/lannymcnie/9ntajsa1/1/
// Canvas to draw to:
var offCanvas = document.getElementById("canvas2");
var offStage = new createjs.Stage(offCanvas);
// Add the offStage to the main stage.
var bmp = new createjs.Bitmap(offCanvas);
stage.addChild(bmp);
bmp.alpha = 0.1;
// Still get events from main stage
stage.addEventListener('stagemousemove',onMouseMove);
Basically, I want to have a large rectangle and cut smaller rectangles out of it. I can draw the rectangle, then do clearRect(x, y, w, h) but it removes stuff I've drawn on previous to even creating the rectangle.
So basically, can I draw a rectangle over an image and then cut the rectangle without affecting the image underneath it? I have a small idea of using ctx.save and restore, but I can't quite get it to work - and I'm not 100% sure that's the right answer.
Any help would be greatly appreciated!
Thanks,
Cam
An alternative might be to use paths:
Call context.beginPath()
Define the outer rect with moveTo and lineTo and closePath, going clockwise
Cut out the holes the same way but, going counter-clockwise
Call context.fill() to fill the rectangle without the holes.
This only works if you know all the holes from the start, though. This answer has more details for this approach:
Polygon with a hole in the middle with HTML5's canvas
Thanks to markE and Stefan for giving a solution. However, after hours of searching I found my own answer based on this answer. I have no idea why it works, but check out what code I used below:
ctx.fillStyle = "grey";
ctx.beginPath();
ctx.rect(100, 100, 50, 50);
ctx.rect(400, 0, -400, 400);
ctx.fill();
For some reason, if you declare what shape you want to cut out first, and then draw the overlaying shape BACKWARDS (?!) over your area, it cuts out the shape. WTF Javascript? Anyways, it works :)
I'm fairly new to javascript and I want to make a small line spectrum using html canvas and javascript so that I can later animate it when a button is clicked to show absorbtion lines and such just like in chemistry. I'm just not sure if pure javascript is able to handle this. Can anyone provide some information on how this could be done or if this is even possible? I can gladly provide more information if needed.
EDIT: Here is what I have so far but I don't know why the color gradient is slanting where the colors change and the widths of the colors need adjusted so they take up equal amounts along the rectangle.
HTML
<canvas id="spectralCanvas" width="600" height="100"></canvas>
JavaScript
function loadSpectralCanvas() {
var spectralCanvas = document.getElementById('spectralCanvas');
var spectralCtx = spectralCanvas.getContext('2d');
var backgroundCtx = spectralCanvas.getContext('2d');
spectralCtx.rect(0, 0, spectralCanvas.width, spectralCanvas.height / 2);
//Creates black underlay to represent absorption lines on the spectrum.
backgroundCtx.fillStyle = 'black';
backgroundCtx.fillRect(0, 0, spectralCanvas.width, spectralCanvas.height / 2);
//Creates the ROY G BIV line spectrum.
var gradient = spectralCtx.createLinearGradient(0, 0, spectralCanvas.width, spectralCanvas.height / 2);
gradient.addColorStop(0.14, '#882022'); //Red
gradient.addColorStop(0.28, '#E83B23'); //Orange
gradient.addColorStop(0.42, '#FFF101'); //Yellow
gradient.addColorStop(0.56, '#89C540'); //Green
gradient.addColorStop(0.70, '#1BBDC9'); //Blue
gradient.addColorStop(0.84, '#0279B9'); //Indigo
gradient.addColorStop(1.0, '#58235E'); //Violet
spectralCtx.fillStyle = gradient;
spectralCtx.fill();
}
loadSpectralCanvas();
JSFiddle
The last argument to the create gradient function should be 0, as this will produce a horizontal gradient that stretches the width of the canvas.
For your colour stops, you may be better off writing 1/7, 2/7 and so on to let JavaScript handle it for you.
It's definitely possible with pure JavaScript. The Mozilla Developer Network is a great resource to start with. They have a canvas tutorial that should be able to get you started in the right direction. Depending on how much experience you have you might consider going straight to the drawing shapes part of the tutorial. Good luck.
I'm trying to understand the combination of HTML5/CSS3 and Javascript more and more.
That's why I thought, make a little project so you learn all about that more.
In short, I like the new iOS7 wallpaper and use it on my website (http://www.betadevelops.com). Then I thought, let's make this more lightweight and draw it with pure Javascript.
I started and managed to get quite far (http://www.betadevelops.com/jOS7.html). But now I face a stupid problem I can't seem to get fixed.
I draw circles on the canvas, and dynamically assign colors to it. But each time a new circle (and so a new color gets chosen) it automatically recolors the old circles...
So let's say, 10 circles:
1: blue circle, draw's it and done
2: yellow circle, draw's it and done, but it also colors the first blue one to yellow
I also wanted to add opacity and blurring. The opacity kinda works in the sense it has opacity on only 2-3 circles from the 20 I draw. I think this is not possible because I use Math.Random the calculate a random opacity.
Considered the blurring, I can add blurring to the whole canvas with follow code
canvas.style.webkitFilter = "blur(3px)";
but that's not what I want. I want the blur on the circle itself and to be more precisely, the outline. I read about it and it's not possible, but you can mimmick the looks with using CSS box-shadow.
So I tried
canvas.style.webkitFilter = "box-shadow(10px 10px 5px #888)";
but this also doesn't work it seems...
So, you website guru's. What am I doing wrong and can you help me out?
You can find the code by clicking on the second link. Uploaded it there.
EDIT:
Nevermind the blur, managed to solve it partially with this code
if (blurred) {
ctx.shadowColor = color;
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
#Stig Runar Vangen has the correct answer.
I would just add that if you don't intend the circles to "run", you could use ctx.closePath after drawing each ctx.arc.
ctx.beginPath();
ctx.arc(centerX, centerY, diameter, 0, 2 * Math.PI, false);
ctx.closePath();
color = color.replace('opacity', Math.random().toString());
ctx.fillStyle = color;
ctx.fill();
The reason why you see that all your circles gets the same color, might be because you join all circles into one draw operation. To separate each circle draw operation, start each circle placement with:
ctx.beginPath();
Each arc should then also be drawn with a call to either ctx.stoke() or ctx.fill() after the definition of each single circle.
This is purely guesswork as I haven't seen your code.
I'm making a top-down shooter game that relies on the avatar always being rotated pointing to the mouse cursor. I achieve rotation like this:
//Rendering.
context.save(); //Save the context state, we're about to change it a lot.
context.translate(position[0] + picture.width/2, position[1] + picture.height/2); //Translate the context to the center of the image.
context.rotate(phi); //Rotate the context by the object's phi.
context.drawImage(picture.image, -picture.width/2, -picture.height/2); //Draw the image at the appropriate position (center of the image = [0, 0]).
context.restore(); //Get the state back.
When the phi is zero, the image is rendered in its normal quality, with sharp edges and detectable pixels. But, when I set the phi to a nonzero value (actually, when it's not 0, Pi/2, Pi, Pi+Pi/2 or 2Pi), the image looses it's sharpness and the individual pixels can't be seen anymore, because they are blurred out.
Here's a screenshot (sorry about the general bad quality of the screenshot, but I think that the difference is more than noticeable):
This is, well, a bit unacceptable. I can't have the images always blurred out! Why is this happening and can I solve it?
You could try
context.imageSmoothingEnabled = false;
See docs:
context.imageSmoothingEnabled [ = value ]
Returns whether pattern fills and the drawImage() method will attempt to smooth images if they have to rescale them (as opposed to just rendering the images with "big pixels").
Can be set, to change whether images are smoothed (true) or not (false).
If you want a true pixel-art retro style effect, you'd need to manually create rotated sprite images for several angles, look up the appropriate sprite for the current value of phi, and draw it without rotation. This obviously requires a fair amount of art work!
IF you are rotating images around their center point, make sure the image itself has an even number of pixels. Once you end up on odd coordinates the image data needs to be interpolated for the target canvas. Apple has some nice documentation on translating and rotating the canvas.
So for any image, as suggested above use rounding to snap to full pixels.
context.translate(Math.floor(img.width/2), Math.floor(img.height/2));
This way every source pixel of your image will always be drawn exactly into a pixel inside the canvas and blurring does not occur. This however is only true for multiples of 90 degrees.
It seems that all browsers do, to some extend, antialiasing in image drawing so you will probably have to provide rotated images as sprites.
According to this Chromium bug report you might be lucky there if they haven't fixed it yet. Read through and you'll learn that Ian Hickson likely opposed making antialiased image drawing optional.
(picture.width/2, picture.height/2) point won't always work.
(Math.floor(picture.width/2) + 0.5, Math.floor(picture.height/2) + 0.5) should help.
Well, actually it is something you cannot get around
If you rotate an image by a multiple of 90 degrees, your library should smart enough so that no interpolation is applied.
But as soon as you rotate an image by an angle different from a multiple of 90 degrees, you need to interpolate. As a consequence, you get that smoothing. If you are interested in the theory, you may look for a book on computer graphics or image processing.
For the concrete case of image rotation you may have a look at this paper,
http://bigwww.epfl.ch/publications/unser9502.html