diff --git a/buildconfig/stubs/pygame/math.pyi b/buildconfig/stubs/pygame/math.pyi
index 4526960b5a..97fb7155dd 100644
--- a/buildconfig/stubs/pygame/math.pyi
+++ b/buildconfig/stubs/pygame/math.pyi
@@ -221,6 +221,10 @@ class Vector2(_GenericVector):
xy: Vector2
yx: Vector2
yy: Vector2
+ @property
+ def angle(self) -> float: ...
+ @property
+ def angle_rad(self) -> float: ...
@overload
def __init__(
self: _TVec,
diff --git a/docs/reST/ref/math.rst b/docs/reST/ref/math.rst
index e359a4b899..e366b1f5c3 100644
--- a/docs/reST/ref/math.rst
+++ b/docs/reST/ref/math.rst
@@ -616,6 +616,27 @@ Multiple coordinates can be set using slices or swizzling
find that either the margin is too large or too small, in which case changing ``epsilon`` slightly
might help you out.
+ .. attribute:: angle
+
+ | :sl:`Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval [-180, 180].`
+
+ Read-only attribute representing the angle of the vector in degrees relative to the X-axis. This angle is normalized to
+ the interval [-180, 180].
+
+ Usage: Accessing `angle` provides the current angle of the vector in degrees within the predefined range of [-180, 180].
+
+ .. versionadded:: 2.5.5
+
+ .. attribute:: angle_rad
+
+ | :sl:`Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval [-π, π].`
+
+ Read-only attribute representing the angle of the vector in radians relative to the X-axis. This value is equivalent
+ to the `angle` attribute converted to radians and is normalized to the interval [-π, π].
+
+ Usage: Accessing `angle_rad` provides the current angle of the vector in radians within the predefined range of [-π, π].
+
+ .. versionadded:: 2.5.5
.. ## pygame.math.Vector2 ##
diff --git a/src_c/doc/math_doc.h b/src_c/doc/math_doc.h
index e002a0c672..548465487d 100644
--- a/src_c/doc/math_doc.h
+++ b/src_c/doc/math_doc.h
@@ -40,6 +40,8 @@
#define DOC_MATH_VECTOR2_CLAMPMAGNITUDEIP "clamp_magnitude_ip(max_length, /) -> None\nclamp_magnitude_ip(min_length, max_length, /) -> None\nClamps the vector's magnitude between max_length and min_length"
#define DOC_MATH_VECTOR2_UPDATE "update() -> None\nupdate(int) -> None\nupdate(float) -> None\nupdate(Vector2) -> None\nupdate(x, y) -> None\nupdate((x, y)) -> None\nSets the coordinates of the vector."
#define DOC_MATH_VECTOR2_EPSILON "Determines the tolerance of vector calculations."
+#define DOC_MATH_VECTOR2_ANGLE "Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval [-180, 180]."
+#define DOC_MATH_VECTOR2_ANGLERAD "Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval [-π, π]."
#define DOC_MATH_VECTOR3 "Vector3() -> Vector3(0, 0, 0)\nVector3(int) -> Vector3\nVector3(float) -> Vector3\nVector3(Vector3) -> Vector3\nVector3(x, y, z) -> Vector3\nVector3((x, y, z)) -> Vector3\na 3-Dimensional Vector"
#define DOC_MATH_VECTOR3_DOT "dot(Vector3, /) -> float\ncalculates the dot- or scalar-product with the other vector"
#define DOC_MATH_VECTOR3_CROSS "cross(Vector3, /) -> Vector3\ncalculates the cross- or vector-product"
diff --git a/src_c/math.c b/src_c/math.c
index 24c04365ca..10238b6b6b 100644
--- a/src_c/math.c
+++ b/src_c/math.c
@@ -51,6 +51,9 @@
#define TWO_PI (2. * M_PI)
+#define RAD_TO_DEG (180.0 / M_PI)
+#define DEG_TO_RAD (M_PI / 180.0)
+
#ifndef M_PI_2
#define M_PI_2 (M_PI / 2.0)
#endif /* M_PI_2 */
@@ -142,6 +145,8 @@ _vector_coords_from_string(PyObject *str, char **delimiter, double *coords,
static void
_vector_move_towards_helper(Py_ssize_t dim, double *origin_coords,
double *target_coords, double max_distance);
+static double
+_pg_atan2(double y, double x);
/* generic vector functions */
static PyObject *
@@ -202,6 +207,10 @@ vector_sety(pgVector *self, PyObject *value, void *closure);
static int
vector_setz(pgVector *self, PyObject *value, void *closure);
static PyObject *
+vector_get_angle(pgVector *self, void *closure);
+static PyObject *
+vector_get_angle_rad(pgVector *self, void *closure);
+static PyObject *
vector_richcompare(PyObject *o1, PyObject *o2, int op);
static PyObject *
vector_length(pgVector *self, PyObject *args);
@@ -631,6 +640,40 @@ vector_dealloc(pgVector *self)
Py_TYPE(self)->tp_free((PyObject *)self);
}
+/*
+ *Returns rhe arctangent of the quotient y / x, in radians, considering the
+ *following special cases: atan2((anything), NaN ) is NaN; atan2(NAN ,
+ *(anything) ) is NaN; atan2(+-0, +(anything but NaN)) is +-0 ; atan2(+-0,
+ *-(anything but NaN)) is +-pi ; atan2(+-(anything but 0 and NaN), 0) is
+ *+-pi/2; atan2(+-(anything but INF and NaN), +INF) is +-0 ; atan2(+-(anything
+ *but INF and NaN), -INF) is +-pi; atan2(+-INF,+INF ) is +-pi/4 ;
+ * atan2(+-INF,-INF ) is +-3pi/4;
+ * atan2(+-INF, (anything but,0,NaN, and INF)) is +-pi/2;
+ *
+ */
+static double
+_pg_atan2(double y, double x)
+{
+ if (Py_IS_NAN(x) || Py_IS_NAN(y)) {
+ return Py_NAN;
+ }
+
+ if (Py_IS_INFINITY(y)) {
+ if (Py_IS_INFINITY(x)) {
+ return copysign((copysign(1., x) == 1.) ? 0.25 * Py_MATH_PI
+ : 0.75 * Py_MATH_PI,
+ y);
+ }
+ return copysign(0.5 * Py_MATH_PI, y);
+ }
+
+ if (Py_IS_INFINITY(x) || y == 0.) {
+ return copysign((copysign(1., x) == 1.) ? 0. : Py_MATH_PI, y);
+ }
+
+ return atan2(y, x);
+}
+
/**********************************************
* Generic vector PyNumber emulation routines
**********************************************/
@@ -1269,6 +1312,23 @@ vector_setz(pgVector *self, PyObject *value, void *closure)
return vector_set_component(self, value, 2);
}
+static PyObject *
+vector_get_angle_rad(pgVector *self, void *closure)
+{
+ double angle_rad = _pg_atan2(self->coords[1], self->coords[0]);
+
+ return PyFloat_FromDouble(angle_rad);
+}
+
+static PyObject *
+vector_get_angle(pgVector *self, void *closure)
+{
+ double angle_rad = _pg_atan2(self->coords[1], self->coords[0]);
+ double angle_deg = angle_rad * RAD_TO_DEG;
+
+ return PyFloat_FromDouble(angle_deg);
+}
+
static PyObject *
vector_richcompare(PyObject *o1, PyObject *o2, int op)
{
@@ -2585,6 +2645,9 @@ static PyMethodDef vector2_methods[] = {
static PyGetSetDef vector2_getsets[] = {
{"x", (getter)vector_getx, (setter)vector_setx, NULL, NULL},
{"y", (getter)vector_gety, (setter)vector_sety, NULL, NULL},
+ {"angle", (getter)vector_get_angle, NULL, DOC_MATH_VECTOR2_ANGLE, NULL},
+ {"angle_rad", (getter)vector_get_angle_rad, NULL,
+ DOC_MATH_VECTOR2_ANGLERAD, NULL},
{NULL, 0, NULL, NULL, NULL} /* Sentinel */
};
diff --git a/test/math_test.py b/test/math_test.py
index d8690ff502..e99f3aa25f 100644
--- a/test/math_test.py
+++ b/test/math_test.py
@@ -1363,6 +1363,104 @@ def test_del_y(self):
exception = ctx.exception
self.assertEqual(str(exception), "Cannot delete the y attribute")
+ def test_angle_rad_property(self):
+ v0 = Vector2(1, 0)
+ self.assertEqual(v0.angle_rad, 0.0)
+
+ v1 = Vector2(0, 1)
+ self.assertEqual(v1.angle_rad, math.pi / 2)
+
+ v2 = Vector2(-1, 0)
+ self.assertEqual(v2.angle_rad, math.pi)
+
+ v3 = Vector2(0, -1)
+ self.assertEqual(v3.angle_rad, -math.pi / 2)
+
+ v4 = Vector2(1, 1)
+ self.assertEqual(v4.angle_rad, math.pi / 4)
+
+ v5 = Vector2(-1, 1)
+ self.assertEqual(v5.angle_rad, 3 * math.pi / 4)
+
+ v6 = Vector2(-1, -1)
+ self.assertEqual(v6.angle_rad, -3 * math.pi / 4)
+
+ v7 = Vector2(1, -1)
+ self.assertEqual(v7.angle_rad, -math.pi / 4)
+
+ v8 = Vector2(float('inf'), float('inf'))
+ self.assertEqual(v8.angle_rad, math.pi / 4)
+
+ v9 = Vector2(float('-inf'), float('inf'))
+ self.assertEqual(v9.angle_rad, 3 * math.pi / 4)
+
+ v10 = Vector2(float('-inf'), float('-inf'))
+ self.assertEqual(v10.angle_rad, -3 * math.pi / 4)
+
+ v11 = Vector2(float('inf'), float('-inf'))
+ self.assertEqual(v11.angle_rad, -math.pi / 4)
+
+ v12 = Vector2(0, 0)
+ self.assertEqual(v12.angle_rad, 0.0)
+
+ v13 = Vector2(float('nan'), 1)
+ self.assertTrue(math.isnan(v13.angle_rad))
+
+ v14 = Vector2(1, float('nan'))
+ self.assertTrue(math.isnan(v14.angle_rad))
+
+ v15 = Vector2(float('nan'), float('nan'))
+ self.assertTrue(math.isnan(v15.angle_rad))
+
+ def test_angle_property(self):
+ v0 = pygame.math.Vector2(1, 0)
+ self.assertEqual(v0.angle, 0.0)
+
+ v1 = pygame.math.Vector2(0, 1)
+ self.assertEqual(v1.angle, 90.0)
+
+ v2 = pygame.math.Vector2(-1, 0)
+ self.assertEqual(v2.angle, 180.0)
+
+ v3 = pygame.math.Vector2(0, -1)
+ self.assertEqual(v3.angle, -90.0)
+
+ v4 = pygame.math.Vector2(1, 1)
+ self.assertEqual(v4.angle, 45.0)
+
+ v5 = pygame.math.Vector2(-1, 1)
+ self.assertEqual(v5.angle, 135.0)
+
+ v6 = pygame.math.Vector2(-1, -1)
+ self.assertEqual(v6.angle, -135.0)
+
+ v7 = pygame.math.Vector2(1, -1)
+ self.assertEqual(v7.angle, -45.0)
+
+ v8 = pygame.math.Vector2(float('inf'), float('inf'))
+ self.assertEqual(v8.angle, 45.0)
+
+ v9 = pygame.math.Vector2(float('-inf'), float('inf'))
+ self.assertEqual(v9.angle, 135.0)
+
+ v10 = pygame.math.Vector2(float('-inf'), float('-inf'))
+ self.assertEqual(v10.angle, -135.0)
+
+ v11 = pygame.math.Vector2(float('inf'), float('-inf'))
+ self.assertEqual(v11.angle, -45.0)
+
+ v12 = pygame.math.Vector2(0, 0)
+ self.assertEqual(v12.angle, 0.0)
+
+ v13 = pygame.math.Vector2(float('nan'), 1)
+ self.assertTrue(math.isnan(v13.angle))
+
+ v14 = pygame.math.Vector2(1, float('nan'))
+ self.assertTrue(math.isnan(v14.angle))
+
+ v15 = pygame.math.Vector2(float('nan'), float('nan'))
+ self.assertTrue(math.isnan(v15.angle))
+
class Vector3TypeTest(unittest.TestCase):
def setUp(self):