9

For a context I won't get into, I need two functions that are essentially reciprocals of each other.

angle_to() should return the number of degrees a clockhand would have to turn to travel from 0° to the line connecting p1 to p2 (ie. p1 is the center of rotation), and where both p1 and p2 are pixel coordinates.

point_pos() should return the pixel coordinates of where a clockhand of length amplitude would be had it turned angle.

For both, the positive x-axis = 0° = 3 o'clock, and the argument rotation should shift that axis before the calculation begins in either the clockwise or counter-clockwise direction; then said calculation should move in the same direction with this adjusted reference.

My progress on each is included below; the failure is:

When clockwise=False, it returns the correct answer for the clockwise condition; when clockwise=True, angle_between() returns the right answer with a rounding error, and point_pos() gives me the wrong answer entirely.

I've also attached a visual explanation I mocked up in Illustrator as an apology to the internet for being unable to solve this and in case what I'm seeking isn't clear.

Edit: cleaned up a line that unnecessarily complicated as per one answer below.

from math import sin, cos, radians, pi, atan2, degrees

def angle_to(p1, p2, rotation=0, clockwise=False):
    if abs(rotation) > 360:
        rotation %= 360
    p2 = list(p2)
    p2[0] = p2[0] - p1[0]
    p2[1] = p2[1] - p1[1]

    angle = degrees(atan2(p2[1], p2[0]))
    if clockwise:
        angle -= rotation
        return angle if angle > 0 else angle + 360
    else:
        angle = (360 - angle if angle > 0 else -1 * angle) - rotation
        return angle if angle > 0 else angle + 360

def point_pos(origin, amplitude, angle, rotation=0, clockwise=False):
    if abs(rotation) > 360:
        rotation %= 360
    if clockwise:
        rotation *= -1
    if clockwise:
        angle -= rotation
        angle = angle if angle > 0 else angle + 360
    else:
        angle = (360 - angle if angle > 0 else -1 * angle) - rotation
        angle = angle if angle > 0 else angle + 360

    theta_rad = radians(angle)
    return int(origin[0] + amplitude * cos(theta_rad)), int(origin[1] + amplitude * sin(theta_rad))

angle_to() point_pos()

Edit #2: Upon request, here's some failed output:

angle_to() is flipping clockwise and counterclockwise (when I've tried to fix it, I end up getting wrong answers altogether), and in the clockwise direction, rotating and calculating in different directions

>>> print angle_to((100,100), (25,25))  # should be 225  
135.0
>>> print angle_to((100,100), (25,25), 45)  # should be 180
90.0
>>> print angle_to((100,100), (25,25), clockwise=True) # should be 135
225.0
>>> print angle_to((100,100), (25,25), 45, clockwise=True)  # should be 90
180.0

point_pos() is just wrong in the counterclockwise direction

# dunno what these should be (i'm bad at trig) but when I visually place the
# given p1 and the output p2 on screen it's obvious that they're wrong
>>> print point_pos((100,100), 75, 225)               
(46, 153)
>>> print point_pos((100,100), 75, 225, 45)
(100, 175)

# these are basically correct, rounding-errors notwithstanding
>>> print point_pos((100,100), 75, 225, clockwise=True)
(46, 46)
>>> print point_pos((100,100), 75, 225, 45, clockwise=True)
(99, 25)
7
  • Could you please explain what is the problem you have with your code ? Commented May 16, 2016 at 17:17
  • 3rd paragraph; it's not handling the clockwise/counterclockwise condition correctly. When clockwise=False, it returns the correct answer for the clockwise condition; when clockwise=True, angle_between() returns the right answer with a rounding error, and point_pos() gives me the wrong answer entirely.
    – Jonline
    Commented May 16, 2016 at 17:29
  • Could you give an example of an input and the expected result (for example with a 90 degree rotation)? Commented May 16, 2016 at 17:41
  • @Han-KwangNienhuys Added to bottom of question.
    – Jonline
    Commented May 16, 2016 at 17:50
  • I still can't tell from your examples what angle_to is supposed to do. One thing though: be aware that your graphics coordinates have the y axis pointing downward, while a standard y axis points upwards. So, angles to use with atan2, cos, sin may be different from what you expect. Commented May 16, 2016 at 18:12

3 Answers 3

7

You can simplify your code quite a bit by using a couple of simple rules. Simple code is less likely to have bugs.

First, converting between clockwise and counter-clockwise just means inverting the sign: angle = -angle.

Second, to restrict an angle to the range [0, 360) you simply use angle % 360. This works no matter if the angle started out negative or positive, integer or floating point.

def angle_to(p1, p2, rotation=0, clockwise=False):
    angle = degrees(atan2(p2[1] - p1[1], p2[0] - p1[0])) - rotation
    if not clockwise:
        angle = -angle
    return angle % 360
3
  • But it doesn't mean simply inverting the sign; travelling 45° clockwise is effectively 315° counterclockwise starting from the same position. It's minimally (360 - angle) % 360.
    – Jonline
    Commented May 17, 2016 at 15:23
  • 1
    @Jonline which is why I tell you to do both. (360 - angle) % 360 is the same as -angle % 360. Commented May 17, 2016 at 15:26
  • ...and jono crosses the finish line. Thanks.
    – Jonline
    Commented May 17, 2016 at 15:29
1

This: angle = (360 - angle if angle > 0 else -1 * angle) - rotation I don't know what you were trying to achieve there, but that indeed does not do what you want. Just having -angle reflects the angle; changes the angle direction, from anti-clockwise to clockwise, noting that you're in the counter-clockwise branch of the condition. Then you add 360, and that messes everything up. The else branch just multiplies the angle by -1 - reversing it again. The clockwise branch is where you needed to reverse the angle (and add 360 to ensure the angle is positive). Here is a simple version of your function fixed without the extra rotation parameter:

def angle_to(p1, p2, clockwise=False):
    p2 = list(p2)
    p2[0] = p2[0] - p1[0]
    p2[1] = (p2[1] - p1[1])
    angle = degrees(atan2(p2[1], p2[0]))
    angle = 360 + angle if angle < 0 else angle
    return angle if not clockwise else -angle+360

Your other function suffers from exactly the same problem in these lines:

if clockwise:
    angle -= rotation
    angle = angle if angle > 0 else angle + 360
else:
    angle = (360 - angle if angle > 0 else -1 * angle) - rotation
    angle = angle if angle > 0 else angle + 360

Should be:

angle -= rotation
if clockwise:
    angle = -angle+360 if angle > 0 else -angle
else:
    angle = angle if angle > 0 else angle + 360
3
  • Ok, this solves almost everything except the argument, rotation, which in-situ is actually critical (I'm developing for experiments in cognitive psych and this sometimes calls for strange requirements). Some of the lines you observed as unclear—no doubt they were, I'm out of my depth—were intended to adjust for the 0° axis being rotated rotatation degrees before calculating. Assuming the solution that accounts for this is the same for both, can you comment to how it should be incorporated?
    – Jonline
    Commented May 17, 2016 at 15:21
  • Though... if this does indeed return the correct angle, it occurs to me that in either direction angle -= rotation. But, since I always want to return a positive angle, am I correct that I'd additionally need return angle if angle > 0 else 360 + angle?
    – Jonline
    Commented May 17, 2016 at 15:27
  • That's correct. Alternatively, you can just surround what the comes after the return statement with parenthesis and use operator % with 360 as @MarkRansom suggested in his answer. That should ensure you always get a positive angle in the range [0, 360[. Commented May 17, 2016 at 15:34
1

Re: "angle_to() should return the number of degrees a clockhand would have to turn to travel from p1 to p2"

In your code, you subtract the coordinates of point p1 from p2 before you calculate the angle using atan2. Essentially, you're considering p1 to be the center of your clock, so it doesn't make any sense to talk about "travelling from p1 to p2 by a rotation". You'll need to specify three points: the center around which you do the rotations, point 1, and point 2. If the coordinates are xc, yc, x1, y1, x2, y2, then you'd need to do something like this:

angle1 = atan2(y1-yc, x1-xc)
angle2 = atan2(y2-yc, x2-xc)
relative_angle = angle1 - angle2
# now convert to degrees and handle +/-360 issues.

Update with your new specification: "return the number of degrees a clockhand would have to turn to travel from 0° to the line connecting p1 to p2":

angle = degrees(atan2(p2[1], p2[0]))

This will return the clockwise angle (in pixel coordinates) in the range -pi to +pi (-180 to +180 deg). In your example, angle_to((100,100), (25,25)) ("want 225, but get 135"), the atan2 will result in -135 deg, which means +135 deg counterclockwise. That is the answer that you would want (modulo 360 degrees), since you have not specified whether the clock hand should be turning cw or ccw (you only specify whether the starting position is cw or ccw relative to the 3 o'clock position). However, depending on the value of clockwise, which defaults to False, you do something complicated.

If you want to ensure that the clock hand turns cw, then you should add 360 deg to the result angle if it is negative, not revert the angle.

(Note: I deleted the old answer; the first two comments refer to the old answer.)

3
  • Confirmed; cleans up the code that still doesn't work.
    – Jonline
    Commented May 16, 2016 at 17:36
  • 1
    Shouldn't matter, as it's just a constant factor you're multiplying both coordinates to (he/she was performing a scale, essentially). Commented May 16, 2016 at 17:38
  • sigh. you're absolutely right, but that's a mistake in my description, which I'll change now; it should read "degrees clockhand would turn to arrive at p2 if p1 were the center of the clock and it were currently pointing at 0°".
    – Jonline
    Commented May 16, 2016 at 20:21

Not the answer you're looking for? Browse other questions tagged or ask your own question.