Source code for arcade.math

import math
import random
from typing import TypeVar

from pyglet.math import Vec2, Vec3

from arcade.types import AsFloat, HasAddSubMul, Point, Point2, SupportsRichComparison
from arcade.types.rect import Rect
from arcade.types.vector_like import Point3

_PRECISION = 2


__all__ = [
    "clamp",
    "lerp",
    "lerp_2d",
    "lerp_3d",
    "lerp_angle",
    "rand_in_rect",
    "rand_in_circle",
    "rand_on_circle",
    "rand_on_line",
    "rand_angle_360_deg",
    "rand_angle_spread_deg",
    "rand_vec_spread_deg",
    "rand_vec_magnitude",
    "get_distance",
    "rotate_point",
    "get_angle_degrees",
    "get_angle_radians",
    "quaternion_rotation",
]

SupportsRichComparisonT = TypeVar("SupportsRichComparisonT", bound=SupportsRichComparison)


[docs] def clamp( a: SupportsRichComparisonT, low: SupportsRichComparisonT, high: SupportsRichComparisonT ) -> SupportsRichComparisonT: """Clamp a number between a range. Args: a (float): The number to clamp low (float): The lower bound high (float): The upper bound """ # Python will deal with > unsupported by falling back on <. return high if a > high else max(a, low) # type: ignore
# This TypeVar helps match v1 and v2 as the same type below in lerp's # signature. If we used HasAddSubMul, they could be different. L = TypeVar("L", bound=HasAddSubMul)
[docs] def lerp(v1: L, v2: L, u: float) -> L: """Linearly interpolate two values which support arithmetic operators. Both ``v1`` and ``v2`` must be of compatible types and support the following operators: * ``+`` (:py:meth:`~object.__add__`) * ``-`` (:py:meth:`~object.__sub__`) * ``*`` (:py:meth:`~object.__mul__`) This means that in certain cases, you may want to use another function: * For angles, use :py:func:`lerp_angle`. * To convert points as arbitary sequences, use: * :py:func:`lerp_2d` * :py:func:`lerp_3d` Args: v1 (HasAddSubMul): The first value v2 (HasAddSubMul): The second value u: The interpolation value `(0.0 to 1.0)` """ return v1 + ((v2 - v1) * u)
[docs] def lerp_2d(v1: Point2, v2: Point2, u: float) -> Vec2: """Linearly interpolate between two 2D points passed as sequences. .. tip:: This function returns a :py:class:`Vec2` you can use with :py:func`lerp` . Args: v1: The first point as a sequence of 2 values. v2: The second point as a sequence of 2 values. u (float): The interpolation value `(0.0 to 1.0)` """ return Vec2(lerp(v1[0], v2[0], u), lerp(v1[1], v2[1], u))
[docs] def lerp_3d(v1: Point3, v2: Point3, u: float) -> Vec3: """Linearly interpolate between two 3D points passed as sequences. .. tip:: This function returns a :py:class:`Vec3` you can use with :py:func`lerp`. Args: v1: The first point as a sequence of 3 values. v2: The second point as a sequence of 3 values. u (float): The interpolation value `(0.0 to 1.0)` """ return Vec3(lerp(v1[0], v2[0], u), lerp(v1[1], v2[1], u), lerp(v1[2], v2[2], u))
[docs] def smerp(v1: L, v2: L, dt: float, h: float) -> L: """ Smoothly interpolate between two values indepentdant of time. use as `a = smerp(a, b, delta_time, 16)`. .. tip:: To find the ideal decay constant (half-life) you can use: `h = -t / math.log2(p)` where p is how close (percentage) you'd like to be to the target value in t seconds. i.e if in 1 second you'd like to be within 1% then h ~= 0.15051 Args: v1: The first value to interpolate from. v2: The second value to interpolate to. dt: The time in seconds that has passed since v1 was interpolated last h: The decay constant. The higher the faster v1 reaches v2. 0.1-25.0 is a good range. """ return v2 + (v1 - v2) * math.pow(2.0, -dt / h)
[docs] def smerp_2d(v1: Point2, v2: Point2, dt: float, h: float) -> Vec2: """ Smoothly interpolate between two sequences of length 2 indepentdant of time. use as `a = smerp_2d(a, b, delta_time, 16)`. .. tip:: To find the ideal decay constant (half-life) you can use: `h = -t / math.log2(p)` where p is how close (percentage) you'd like to be to the target value in t seconds. i.e if in 1 second you'd like to be within 1% then h ~= 0.15051 .. tip:: This function returns a :py:class:`Vec2` you can use with :py:func`smerp`. Args: v1: The first value to interpolate from. v2: The second value to interpolate to. dt: The time in seconds that has passed since v1 was interpolated last h: The decay constant. The lower the faster v1 reaches v2. 0.1-25.0 is a good range. """ x1, y1 = v1 x2, y2 = v2 d = math.pow(2.0, -dt / h) return Vec2(x2 + (x1 - x2) * d, y2 + (y1 - y2) * d)
[docs] def smerp_3d(v1: Point3, v2: Point3, dt: float, h: float) -> Vec3: """ Smoothly interpolate between two sequences of length 3 indepentdant of time. use as `a = smerp_3d(a, b, delta_time, 16)`. .. tip:: To find the ideal decay constant (half-life) you can use: `h = -t / math.log2(p)` where p is how close (percentage) you'd like to be to the target value in t seconds. i.e if in 1 second you'd like to be within 1% then h ~= 0.15051 .. tip:: This function returns a :py:class:`Vec3` you can use with :py:func`smerp`. Args: v1: The first value to interpolate from. v2: The second value to interpolate to. dt: The time in seconds that has passed since v1 was interpolated last h: The decay constant. The higher the faster v1 reaches v2. 0.1-25.0 is a good range. """ x1, y1, z1 = v1 x2, y2, z2 = v2 d = math.pow(2.0, -dt / h) return Vec3(x2 + (x1 - x2) * d, y2 + (y1 - y2) * d, z2 + (z1 - z2) * d)
[docs] def lerp_angle(start_angle: float, end_angle: float, u: float) -> float: """ Linearly interpolate between two angles in degrees, following the shortest path. Args: start_angle (float): The starting angle end_angle (float): The ending angle u (float): The interpolation value (0.0 to 1.0) """ start_angle %= 360 end_angle %= 360 while start_angle - end_angle > 180: end_angle += 360 while start_angle - end_angle < -180: end_angle -= 360 return lerp(start_angle, end_angle, u) % 360
[docs] def rand_in_rect(rect: Rect) -> Point2: """ Calculate a random point in a rectangle. Args: rect (Rect): The rectangle to calculate the point in. """ return ( random.uniform(rect.left, rect.right), random.uniform(rect.bottom, rect.top), )
[docs] def rand_in_circle(center: Point2, radius: float) -> Point2: """ Generate a point in a circle, or can think of it as a vector pointing a random direction with a random magnitude <= radius. Reference: https://stackoverflow.com/a/50746409 Args: center (Point2): The center of the circle radius (float): The radius of the circle """ # random angle angle = 2 * math.pi * random.random() # random radius r = radius * math.sqrt(random.random()) # calculating coordinates return r * math.cos(angle) + center[0], r * math.sin(angle) + center[1]
[docs] def rand_on_circle(center: Point2, radius: float) -> Point2: """ Generate a point on a circle. Args: center (Point2): The center of the circle radius (float): The radius of the circle """ angle = 2 * math.pi * random.random() return radius * math.cos(angle) + center[0], radius * math.sin(angle) + center[1]
[docs] def rand_on_line(pos1: Point2, pos2: Point2) -> Point: """ Given two points defining a line, return a random point on that line. Args: pos1 (Point2): The first point pos2 (Point2): The second point """ u = random.uniform(0.0, 1.0) return lerp_2d(pos1, pos2, u)
[docs] def rand_angle_360_deg() -> float: """ Returns a random angle in degrees between 0.0 and 360.0. """ return random.uniform(0.0, 360.0)
[docs] def rand_angle_spread_deg(angle: float, half_angle_spread: float) -> float: """ Returns a random angle in degrees, within a spread of the given angle. Args: angle (float): The angle to spread from half_angle_spread (float): The half angle spread """ s = random.uniform(-half_angle_spread, half_angle_spread) return angle + s
[docs] def rand_vec_spread_deg(angle: float, half_angle_spread: float, length: float) -> Point2: """ Returns a random vector, within a spread of the given angle. Args: angle (float): The angle to spread from half_angle_spread (float): The half angle spread length (float): The length of the vector """ a = rand_angle_spread_deg(angle, half_angle_spread) vel = Vec2.from_polar(a, length) return vel.x, vel.y
[docs] def rand_vec_magnitude( angle: float, lo_magnitude: float, hi_magnitude: float, ) -> Point2: """ Return a vector of randomized magnitude pointing in the given direction. Args: angle (float): The vector angle in radians lo_magnitude (float): The lower magnitude hi_magnitude (float): The higher magnitude """ mag = random.uniform(lo_magnitude, hi_magnitude) vel = Vec2.from_polar(angle, mag) return vel.x, vel.y
[docs] def get_distance(x1: float, y1: float, x2: float, y2: float) -> float: """ Get the distance between two points. Args: x1 (float): x coordinate of the first point y1 (float): y coordinate of the first point x2 (float): x coordinate of the second point y2 (float): y coordinate of the second point """ return math.hypot(x1 - x2, y1 - y2)
[docs] def rotate_point( x: float, y: float, cx: float, cy: float, angle_degrees: float, ) -> Point2: """ Rotate a point around a center. Args: x (float): x value of the point you want to rotate y (float): y value of the point you want to rotate cx (float): x value of the center point you want to rotate around cy (float): y value of the center point you want to rotate around angle_degrees (float): Angle, in degrees, to rotate """ temp_x = x - cx temp_y = y - cy # now apply rotation angle_radians = math.radians(angle_degrees) cos_angle = math.cos(angle_radians) sin_angle = math.sin(angle_radians) rotated_x = temp_x * cos_angle + temp_y * sin_angle rotated_y = -temp_x * sin_angle + temp_y * cos_angle # translate back x = round(rotated_x + cx, _PRECISION) y = round(rotated_y + cy, _PRECISION) return x, y
# scale around point
[docs] def rescale_relative_to_point(source: Point2, target: Point2, factor: AsFloat | Point2) -> Point2: """ Calculate where a point should be when scaled by the factor realtive to the source point. Args: source: Where to scaled from. target: The point being scaled. factor: How much to scale by. If factor is less than one, target approaches source. Otherwise it moves away. A factor of zero returns source. Returns: The rescaled point. """ if isinstance(factor, float | int): if factor == 1.0: return target scale_x = scale_y = factor else: try: scale_x, scale_y = factor if scale_x == 1.0 and scale_y == 1.0: return target except ValueError: raise ValueError( "factor must be a float, int, or tuple-like which unpacks as two float-like values" ) except TypeError: raise TypeError( "factor must be a float, int, or tuple-like unpacks as two float-like values" ) dx = target[0] - source[0] dy = target[1] - source[1] return source[0] + dx * scale_x, source[1] + dy * scale_y
[docs] def rotate_around_point(source: Point2, target: Point2, angle: float): """ Rotate a point around another point clockwise. Args: source: The point to rotate around target: The point to rotate angle: The degrees to rotate the target by. """ if source == target or angle % 360.0 == 0.0: return target diff_x = target[0] - source[0] diff_y = target[1] - source[1] r = math.radians(angle) c, s = math.cos(r), math.sin(r) dx = diff_x * c - diff_y * s dy = diff_x * s + diff_y * c return target[0] + dx, target[1] + dy
[docs] def get_angle_degrees(x1: float, y1: float, x2: float, y2: float) -> float: """ Get the angle in degrees between two points. Args: x1 (float): x coordinate of the first point y1 (float): y coordinate of the first point x2 (float): x coordinate of the second point y2 (float): y coordinate of the second point """ x_diff = x2 - x1 y_diff = y2 - y1 return -math.degrees(math.atan2(y_diff, x_diff))
[docs] def get_angle_radians(x1: float, y1: float, x2: float, y2: float) -> float: """ Get the angle in radians between two points. Args: x1 (float): x coordinate of the first point y1 (float): y coordinate of the first point x2 (float): x coordinate of the second point y2 (float): y coordinate of the second point """ x_diff = x2 - x1 y_diff = y2 - y1 return math.atan2(x_diff, y_diff)
[docs] def quaternion_rotation(axis: Point3, vector: Point3, angle: float) -> tuple[float, float, float]: """ Rotate a 3-dimensional vector of any length clockwise around a 3-dimensional unit length vector. This method of vector rotation is immune to rotation-lock, however it takes a little more effort to find the axis of rotation rather than 3 angles of rotation. Ref: https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html. Args: axis (tuple[float, float, float]): The unit length vector that will be rotated around vector (tuple[float, float, float]): The 3-dimensional vector to be rotated angle (float): The angle in degrees to rotate the vector clock-wise by """ _rotation_rads = -math.radians(angle) p1, p2, p3 = vector a1, a2, a3 = axis _c2, _s2 = math.cos(_rotation_rads / 2.0), math.sin(_rotation_rads / 2.0) q0, q1, q2, q3 = _c2, _s2 * a1, _s2 * a2, _s2 * a3 q0_2, q1_2, q2_2, q3_2 = q0**2, q1**2, q2**2, q3**2 q01, q02, q03, q12, q13, q23 = q0 * q1, q0 * q2, q0 * q3, q1 * q2, q1 * q3, q2 * q3 _x = p1 * (q0_2 + q1_2 - q2_2 - q3_2) + 2.0 * (p2 * (q12 - q03) + p3 * (q02 + q13)) _y = p2 * (q0_2 - q1_2 + q2_2 - q3_2) + 2.0 * (p1 * (q03 + q12) + p3 * (q23 - q01)) _z = p3 * (q0_2 - q1_2 - q2_2 + q3_2) + 2.0 * (p1 * (q13 - q02) + p2 * (q01 + q23)) return _x, _y, _z