ISSUES V2.0: angles still suck

@Ignatz To be clear, I meant that your blog post conflates two issues. One is that angles aren’t unique, the other is that the trigonometric functions are not injective on the unit circle. In particular, 240 and -120 are in the same direction.

@Andrew_Stacey - clearly I need to understand what injective means, for a start.

Perhaps a simple example would help, if you have time #-o

@Ignatz here’s how I see it

function setup()
    for i=0,360,60 do
        x=math.cos(math.rad(i))
        y=math.sin(math.rad(i))
        ang=math.deg(math.atan2(y,x))
        if ang<0 then ang = ang + 360 end
        print(i,ang,math.deg(math.atan(y/x)))
    end
end

From my rusty mathematics atan will give you the smaller of the two angles between the line and either the x or y axis. Atan2 gives you the angle to the x axis, but flips to negative past 180 degrees - to get the angle positive, just add 360 (a full circle). I know this’ll probably upset the purists, but this way of thinking helps me get my head around it

@West - That’s exactly what I was doing, but Andrew says it has a problem - or maybe it’s in my explanation that went with it, I’ll wait and see.

…and of course I meant between the x axis in both positive and negative directions rather than x and y axis! Much harder to explain without a diagram :-/

I think it might be the explanation in your blog

@West - oh well, I can only learn something new from making mistakes… Why does everything involve math? :-?

Let me expand on my earlier comment (I was typing on my iPad, hence the brevity). There are two basic issues with using angles:

  1. There are a multitude of angles that represent the same direction (this is what I mean by saying that the function that takes an angle to a direction is not injective) and when one goes around the circle back to where one starts, the angle has shifted by 360 degrees (I’ll use degrees for familiarity). It is actually the second of these that causes the problem.
  2. Knowing one of tan(?), cos(?), or sin(?) is not enough to know the direction that ? points in.

The resolution of the second is simple: use atan2 instead of atan. It corrects for the quadrant so gives exactly the correct angle.

However, because of the first then that angle might not be the angle that we want. It depends entirely on what we are going to do with that angle. If we’re going to compare it with a known angle then we need to think carefully about how we’re going to compare them. The naïve way to compare two angles is to see if one is bigger or less than the other. This can produce bizarre results because I can always add a multiple of 360 to one or other of the angles without changing the direction that they represent to swap their order.

What we usually want to know is how to get from one direction to the other through the shortest angle. We can do that using a naïve comparison providing that we know that the two angles are already within 180 degrees of each other. This ensures that we are comparing the two shortest possible arcs between the two directions: the shortest clockwise with the shortest anticlockwise.

But note that it is not enough to ensure that both angles are within 180 degrees of some fixed angle, say 0. If one is 179 and the other is -179 then the shortest arc between them is the 2 degree arc, not the 358 degree one. So if one angle is already known and the other new, adjust the new one until it is within 180 degrees of the known one.

This is what @Ignatz’ code a little above almost does:

local prevAngle=angle
preangle = (math.atan((offsety)/(offsetx)))
if preangle-prevAngle<-math.pi then preangle=preangle+2*math.pi
elseif preangle-prevAngle>math.pi then preangle=preangle-2*math.pi end

By comparing the difference between preangle and prevAngle and ensuring that it lies within math.pi (we’re now using radians, also there’s a little bit behind the scenes that says that we’re only ever going to be one lot of 2 math.pi out so we don’t need to worry about higher multiples) then Ignatz is ensuring that when we compare angle with preangle, we can be sure that we’re going to go in the right direction.

The only problem with this code is that it uses math.atan instead of math.atan2 and so the angle given by preangle might not be the actual angle but in the opposite direction. Putting in math.atan2(offsety,offsetx) in the second line should fix it.

(That said, I haven’t seen the error yet in the original code when I run it so I can’t be absolutely sure that this is the correct fix. But based on the comments above, it ought to be.)

As a postscript, I’d also say that I suspect that what you are doing can be done without explicitly computing the angles. My view is that one should only compute actual angles as a last resort, and if there’s a way to avoid it then do so.

@Andrew_Stacey - thanks for that explanation, that’s clear to me.+

I meant to use atan2 (and I did, in my blog post), as I’m aware it gives more accurate results than atan.

What I’m trying to do with these posts and explanations is find a logical reason for the sometimes strange looking fixes that are suggested for problems like this. Why add or subtract 360 degrees? If your math is rusty, like most of us, the explanation is not immediately obvious. So I will keep trying to find explanations, and probably keep on making mistakes, but if it helps other people understand, it’s worth it.

Picking up on your final point, I’m sure most Codea users use angles for this kind of thing because we don’t know of an alternative. If you have one, we’d love to know it! ;:wink:

@Andrew_Stacey - I suppose that means I’ll be getting another invoice for math services :wink:

Seriously, though, I (and, I’m sure, all of us) appreciate all the time you spend in here, straightening us out. It really helps to have a mathematician on board.

Just to add my thanks to both @Andrew_Stacey and @Ignatz for your time and explanations. I rapidly get lost in Andrew’s words, as I do in lots of other people’s, and it’s often people like Ignatz that help a lot of us understand or at least use some of the higher wisdom provided.

Having said that, I did understand all of Andrew’s post above! :smiley:

@time_trial - actually, I know all the answers, I just pretend to be ignorant

(that’s my story, anyway) :smiley:

I should just rename this topic - “Everyone is confused in some way by angles here [with the exception of Andrew_Stacey], so join the confusion” XD

If @Ignatz knows the answer, why the farts didn’t he just tell me the answer. He’s a butt :stuck_out_tongue:

@Monkeyman32123 - now, now, no need for name calling. I just want to make you think, that’s all… :wink:

At least, that’s how I wish it was. Actually, I’m down in the mud with you.

Haha, I know, I’m just messing with you. I THINK I finally figured it out (by figured it out I mean I entered random buttons a lot and it seems to work now). But last time I thought that it ended up being even more broken XD
And I’m glad I’m not the only one in the mud, you’ve always been there for me ;-;

ive found and isolated what I believe is the problem with my code y’all! I need to find a logical comparison (or series of comparisons) for the if statement below that determines which arc is smaller: the arc (in radians) from anglerad to tangent or the arc from anglerad to tangent2. all three of these values that need comparing will be between 0 and 2*math.pi. I simply need a logical comparison which will find the smaller arc. If the arc from anglerad to tangent is smaller then it needs to choose tangent, which is the counter-clockwise rotation. If the arc from anglerad to tangent2 is smaller then it needs to do what is in the else statement (go to the clockwise tangent, and set a clockwise rotation for addanglerad). I am 90% sure that that is the problem, so I’m going to be superbly disappointed if it isn’t.

        if [insert the comparison here, this is the part I need!] then
            anglerad = tangent
        else
            anglerad = tangent2
            angleaddrad = -angleaddrad
        end

I’ve now taken a closer look at your code. There’s a fair amount of unnecessary stuff in there (and some stuff missing - ever heard of apostrophes?!). Mainly, you keep renormalising your angles in to the range [0,2π) when you don’t need to. This is only necessary when you compare angles, otherwise it has no actual effect. Also, it’s an unfortunate side effect of the Codea functions that you need both the degrees and radians versions of your angles, but by computing both all the time you effectively duplicate your effort. Just stick to one and compute the other when you actually need it.

To your actual problem. It’s simplest to think of this in terms of “modular arithmetic”, so we’ll work with integers but the same principle holds for real numbers.

You have two integers, say a and b, and a period, say 2n. You want to know if a is closer to b than b + n. However, you’re only interested in these numbers modulo 2n. So you don’t want to compare |a - b| with |a - (b + n)|, rather you want to allow for these numbers to vary by multiples of 2n.

That is, you want to compare the minimum of |(a + k 2n) - (b + l 2n)| with the minimum of |(a + k 2n) - (b + n + l 2n)| where k and l range over all integers. We can rearrange these to |(a - b) + (k - l) 2n| and |(a - b) + n + (k - l) 2n|. Since k and l range over all integers, we can replace k - l by j and let j range over all integers; since a and b are known we can write c for a-b. Thus we’re comparing the minimum of |c + j 2n| with |c + n + j 2n|.

Now if we think of this on the number line, c marks a point and c + j 2 n are all the points we get if we start at c and mark every 2nth point in either direction. Similarly, c + n + j 2n are all the points we get if we start at c + n and mark every 2nth point in either direction. The set of all points is thus what we get if we start at c and mark every nth point in each direction. Moreover, the two families alternate as we go up the number line.

Something like: -@-*-@-*-@-*- where the separation is n.

We want to see whether a @ or a * is closest to 0. Since the separation is n, the two closest to 0 will be in the range [-n,n): one above (or equal to 0) and one below. As they are n apart, the one above will be the closest if it is in [0,n/2), otherwise the one below will be the closest.

This suggests a test: translate c and c + n into the range [-n,n) and then see if either is in [0,n/2). If it is, pick that one; if not, pick the other.

To translate them in to that range, we use the modulus function. The integer c%(2n) is the result of translating c into the range [0,2n). To get it into the range [-n,n) we do (c+n)%(2n) - n. So we work with (c+n)%(2n) - n and c%(2n) - n. Now we want to find out if one of these is in the range [0,n/2). We can simply test both for this.

Or we can be a bit clever. It’s enough to test one of them. Let’s work with d = (c+n)%(2n) - n (remember: this is equivalent to c). The other one will be either d + n or d - n, depending on whether d < 0 or not. We have four possibilities:

  1. d is in the range [-n,-n/2). Then the other one must be d + n and is in [0,n/2) so we pick c+n.
  2. d is in the range [-n/2,0). Then the other one must be d + n and is in [n/2,n) so we pick c.
  3. d is in the range [0,n/2). Then we pick c.
  4. d is in the range [n/2,n). Then we pick c + n.

Thus we can simply test -n/2 <= d < n/2. If this is true, pick c; if false, pick c + n.

Substituting in, we look at -n/2 <= (c+n)%(2n) - n < n/2. If we add n/2 to everything, we’re looking at 0 <= (c+n)%(2n) - n/2 < n. Now, it takes a little thought to see this but we can actually take the n/2 inside the %(2n). This is because the part that goes below 0 now ends up above n so still fails the test. That is, the above is equivalent to the test 0 <= (c + n/2)%(2n) < n. The point of doing this is that the result of taking the modulus is always positive, so the test is now simply (c + n/2)%(2n) < n.

In your case, n = math.pi, and c = anglerad - tangent so you’re testing anglerad - tangent + math.pi/2. But tangent = anglebet - math.pi/2 so in fact it’s enough to test (anglerad - anglebet)%(2*math.pi) (though this substitution means that we’re math.pi out: anglerad - anglebet = anglerad - tangent - math.pi/2 but this simply means that we negate the results of the test).

Here’s that in working code. I also tidied up a few things.

function setup()
    strokeWidth(10)
    stroke(255, 0, 0, 139)
    lasso, originid = nil, nil
    --the velocity of the ship simplified and in vector form, both have their uses
    simpveloc = math.pi
    shipVelocity = vec2(simpveloc,0)
    displayMode(FULLSCREEN)
    --starting location and angle for the ship; dont ask why i chose 45 degrees, man, its just how i do
    shiploc = vec2(WIDTH/2, HEIGHT/2)
    anglerad = math.pi/4
end

function draw()
    --make sure the last drawing is covered up, courtesy of Ignatz (hes going to have more credit than me in these comments, and i greatly appreciate it)
    background(0,0,0,255)
    --call the mystical ship location calculating faeries (the function itself is boring, so i spiced up its description)
    calcshiploc()
    --draw the lasso's line. 
    if lasso ~= nil and originid ~= nil then
        strokeWidth(10)
        line(lasso.x,lasso.y, shiploc.x,shiploc.y)
    end
    strokeWidth(0)
    --havent done the push and pop matrix thing yet, sorry, im too focused on that other thing that doesnt work. ill get there. till then, manual rotation
    translate(shiploc.x,shiploc.y)
    --that ellipse is just there to show the area where you may not put the lasso's origin (more on that in calcshiploc())
    ellipse(0,0,60,60)
    rotate(math.deg(anglerad)-90)
    sprite("Space Art:Red Ship", 0,0, 30)
end

function touched(touch)
    --Decide if the lasso should or should not be being drawn (that sentence sucked)
    --makes sure only the original touch affects the lasso, and that there can be only one lasso (FUTURE MULTI-TOUCH FUNCTIONALITY YOU ASK? why yes, thank you for asking, im so glad you observed that)
    if touch.state == BEGAN and originid == nil then
        initlasso(touch)
    elseif touch.state == ENDED and touch.id == originid then
        lasso = nil
        originid = nil
    end
end

function initlasso(t)
    local lassolength = vec2(t.x,t.y):dist(shiploc)
    if lassolength < 30 then
        lasso = nil
        originid = nil
        return
    end
    originid = t.id
    lasso = vec2(t.x,t.y)
    --calculate the circumference of the circle
    local circumf = 2*math.pi*lassolength
    --calculate the number of degrees rotation per draw necessary to move at simpveloc (the ships velocity) [this took me forever to figure out; i was proud]
    angleaddrad = 2*math.pi*simpveloc/circumf
    --calculate the offset in both the x and y axes from the ship to the lasso
    local offset = lasso - shiploc
    --get the angle (in radians) between the ship and the lasso. returns a value from 0- (2*math.pi) [not the best code, i realise, but its easy for me to understand, and it works, so, hey, lay off the scoffing]
    local anglebet = math.atan2(offset.y,offset.x)
    
    --find the two tangent angles of the lasso, one for clockwise and one for anti-clockwise
    local tangent = anglebet - (math.pi/2)
    --supposed to find [and confirm] which direction (clock or anti-clock) the ship should go based on the direction the ship is going and the angle between the ship and the lasso. currently doesnt work, but this one works best of all of my attempts, so im calling it a personal victory
    if (anglerad - anglebet)%(2*math.pi) > math.pi then
        anglerad = tangent
    else
        anglerad = tangent + math.pi
        angleaddrad = -angleaddrad
    end
end

function calcshiploc()
    if lasso ~= nil then
        --if not setting up the lasso (aka it isnt just now being touched), then this is run, which keeps the ship moving in nice pretty circles, unless the velocity is too high, then theyre nice pretty eggs, but considering i never plan to go above 10 velocity, no sweat, plus eggs are healthy, so theres that.
        anglerad = anglerad+ angleaddrad
    end
    --update the ships location based on its rotation or lack thereof
    shiploc = shipVelocity:rotate(anglerad) + shiploc
end

I’m fairly certain you are Merlin.

Also, I know my code was sloppy and full of unnecessary bits and roundabout ways but that’s because it was what I understood, y’know? Your code, in accordance with my Merlin reference, just looks like wizardry to me XD
That said, I will spend the next [long amount of time] dissecting it and drawing diagrams until I figure it out, so, thank you :smiley:
EDIT: nevermind, I see you just cleaned up a lot of the stuff I was far too lazy to. Now I see that the only part I don’t understand it this;

(anglerad - anglebet)%(2*math.pi) > math.pi

@Monkeyman32123 - you better figure it out, otherwise you will be turned into a frog