14

I have a circle which orbits every 10 seconds. And i am trying to cast a shadow which is angled towards the orbit origin (the light source) whilst also taking into account the camera angle as well.

The shadow works for some angles but as the camera goes more edge on or more top down, it starts to look less accurate and I have no idea how to correct for it - it seems like a complicated math issue that I am struggling to figure out how to solve.

This is the animation: http://jsfiddle.net/8y2bm88w/

And my draw code for the shadow:

ctx.beginPath();

//rotate shadow with the planet
ctx.translate(originX + obj[i].x, originY + obj[i].y);
ctx.rotate(obj[i].angle); //rotate around origin
ctx.translate(-(originX + obj[i].x), -(originY + obj[i].y));


var offsetX = -(10 * Math.sin(obj[0].angle)); //i feel this is the issue
var offsetY = 0; //this too

ctx.rect(originX + obj[i].x + offsetX, originY + obj[i].y + offsetY - 10, 20, 20);
ctx.fillStyle = 'rgba(213,0,0,0.9)'; //red shadow - easier to see
ctx.fill();

The code makes more sense via the JSFiddle as it puts the code into more context.

So i think this is to do with the maths for offsetX and offsetY variables, as the user changes the camera angle the offset's need to accommodate and change the way in which the shadow moves. But, this is really confusing how to solve.

Tushar
  • 85,780
  • 21
  • 159
  • 179
Sir
  • 8,135
  • 17
  • 83
  • 146
  • So, if I understand correctly, you want the hemisphere of the circle facing the origin to be illuminated, and the rest to be dark. This is true regardless of camera angle. Correct? – Asad Saeeduddin Nov 03 '15 at 04:06
  • @AsadSaeeduddin it should take into account the angle you are viewing the planet. So i believe you have understood correctly. – Sir Nov 03 '15 at 04:07
  • @AsadSaeeduddin good point, basically it just needs to look correct from the eyes of the viewer. If you look edge on `camera angle = 0` it should cast full shadow and no shadow etc, top down `camera angle = 1` it would cast half shadow all the time. – Sir Nov 03 '15 at 04:09
  • Maybe something like.. `ctx.rect(originX + obj[i].x + offsetX * (1 - camera.angle), originY + obj[i].y - 10, 20, 20);` [**demo**](http://jsfiddle.net/8y2bm88w/1/).. if you want to do curved edges this gets way more complicated and you may even want to consider webgl instead of 2d – Paul S. Nov 03 '15 at 04:53
  • @PaulS. if you look at the shadow from a camera angle of 0 in your demo (side on) that method doesn't work as the shadow jumps incorrectly. And i don't require a curved shadow. Straight is fine. – Sir Nov 03 '15 at 04:58
  • @Dave was also looking to see if something like this would work http://jsfiddle.net/8y2bm88w/2/ (uncomment clip for effect) but I'm too tired to figure out how to fix the line changing width and moving it down with camera angle etc. Good luck! -- edit: well one of those things was easy anyway `ctx.translate(0, -30 * (1 - camera.angle))` – Paul S. Nov 03 '15 at 05:22
  • @Paul interesting idea, though i imagine that would cause issues if you draw other sprites on the canvas should some one wanted to add more to it for example. – Sir Nov 03 '15 at 05:45
  • I think this [equation for equal movement along an ellipse](http://stackoverflow.com/a/26779231/2521214) may shine some light on this ... And this [atmospheric scattering](http://stackoverflow.com/a/19659648/2521214) might be interesting for you – Spektre Nov 03 '15 at 15:45

1 Answers1

7

You're hitting this because your obj[i].angle is based on the ellipse coördinates and not the x-y coördinates of the top-down view (which is actually what you're generating everything else from so we don't need to do calculations for it).

You will need both angles, so I've added a second property for the other angle. The ellipse angle is needed for the rotation of the context so you can be perpendicular to the vector from the origin as viewed. The new angle is for the calculations. You can think of it as a non-2D-mapped version.

obj[i].trueAngle = angle;

Now we have the value to work with, I suggest drawing a semi-circle for the "dark side" and then adding to or cutting out from this region as required using a curve of your choice to complete the shadow, e.g.

// centre on planet
ctx.translate(originX + obj[i].x, originY + obj[i].y);
ctx.rotate(obj[i].angle - Math.PI / 2);
// semicircle dark side
ctx.arc(0, 0, 12, 0, Math.PI, false);
// path along terminator
var offset_terminator = -20 * Math.sin(obj[0].trueAngle) * (1 - camera.angle);
ctx.bezierCurveTo(-7, offset_terminator, 7, offset_terminator, 12, 0);
//fill
ctx.fillStyle = 'rgba(213,0,0,0.9)'; //red shadow - easier to see
ctx.fill();

Here I also made the shadow a couple of px bigger than the planet, I just played with numbers until I found some that looked good.

Please note I set up rotation so 0, 0 is the middle of the planet and the x-axis moves along the terminator

DEMO

Paul S.
  • 64,864
  • 9
  • 122
  • 138
  • If you want more thin crescents, consider reducing the scalar of `offset_terminator` from `-20` to something like `-14`, it actually makes it look quite pretty – Paul S. Nov 03 '15 at 12:14
  • Why the magic numbers 12 and 7? Shouldn't they be 10 and 5 respectively? I gather it's to account for the stroke, but wouldn't it make more sense to just get rid of the stroke and expand the radius of the circle instead? – Fuzzy Logic Nov 03 '15 at 13:25
  • 1
    @FuzzyLogic I chose `12` to make sure that the shadow colour is over all pixels even when the edges are blended as they may not go perfectly at `10` pixels. OP has an outline that would hide this though. `11` wouldn't work on both sides. As for `7`, because it gave a nice shape bezier curve. – Paul S. Nov 03 '15 at 15:26
  • Aha, anti-aliasing. Well done then. From your post, it sounded like you were being whimsical :) – Fuzzy Logic Nov 03 '15 at 15:47
  • Wow this is great @PaulS. ! I don't know what you meant by the offset terminator however, i'm guessing that is relating to some real life effect of shadows, but it looks so good! You're really good at maths :P – Sir Nov 03 '15 at 19:17
  • @Dave the day-night terminator is the line where the day-side meets the night-side. I'm using `offset_terminator` as a number describing how far across the planet to draw the shadow's leading edge in the bezier curve (`0` means straight down the middle) – Paul S. Nov 03 '15 at 19:25
  • @PaulS. sorry for the late reply. Regarding the `12` and `7` as earlier mentioned. Are those chosen related to the size of the planet. For example, if the planet was a different size smaller or larger, is there an easy formula that would allow me to adjust those values so it will still work effectively, same for the `-20` – Sir Nov 05 '15 at 23:41
  • @Dave `12` and `7` are because of the size of the planet, yes. `12` is the radius of the planet (`10`) plus `2`, i'm not sure on the best formula for the other value, maybe something like `Math.floor(3 * (radius + 2) / 5)` will give good values? As for the `-20`, any value greater than `-(radius + 2)` is good, it scales how quickly the shadow passes over the planet, closer to `0` gives more crescents, which can be pretty. If you read my comment I actually suggested changing it to `-14` for the example, so maybe `radius + 4`? – Paul S. Nov 06 '15 at 03:19
  • I thought 10 was the radius. So was a bit confused. But i managed to come up with a decent set of numbers taking into account radius. Just had to tweak it until it looked good ! Now i'm working on solutions for when the orbit is rotated on the x angle rather than the y angle for situations where it is not perpendicular to the origin. Wish me luck :D – Sir Nov 06 '15 at 03:22
  • @Dave `r` _radius_, `r = 10`, `12 = r + 2`. Ah, good to know! May Cthulhu be with you on your code quest! – Paul S. Nov 06 '15 at 03:22