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))
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)
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
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
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.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With