Source code for arcade.math

from __future__ import annotations

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