From afd913a91066dab5a0d7f237629482af9ce9a346 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Thu, 18 Dec 2025 10:49:54 -0500 Subject: [PATCH 01/15] new anim folder, new easings --- arcade/anim/__init__.py | 3 + arcade/anim/easing.py | 328 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 arcade/anim/__init__.py create mode 100644 arcade/anim/easing.py diff --git a/arcade/anim/__init__.py b/arcade/anim/__init__.py new file mode 100644 index 000000000..4a7bb1e70 --- /dev/null +++ b/arcade/anim/__init__.py @@ -0,0 +1,3 @@ +from .easing import ease, Easing, lerp, perc + +__all__ = ["ease", "Easing", "lerp", "perc"] diff --git a/arcade/anim/easing.py b/arcade/anim/easing.py new file mode 100644 index 000000000..ecdfe7231 --- /dev/null +++ b/arcade/anim/easing.py @@ -0,0 +1,328 @@ +from collections.abc import Callable +from math import cos, pi, sin, sqrt, tau +from typing import Protocol, TypeVar + +T = TypeVar("T") + + +class Animatable(Protocol): + def __mul__(self: T, other: T | float, /) -> T: ... + + def __add__(self: T, other: T | float, /) -> T: ... + + def __sub__(self: T, other: T | float, /) -> T: ... + + +A = TypeVar("A", bound=Animatable) + +# === BEGIN EASING FUNCTIONS === + +C1 = 1.70158 +C2 = C1 * 1.525 +C3 = C1 + 1 +TAU_ON_THREE = tau / 3 +TAU_ON_FOUR_AND_A_HALF = tau / 4.5 +N1 = 7.5625 +D1 = 2.75 + + +def _ease_linear(t: float) -> float: + return t + + +def _ease_in_sine(t: float) -> float: + """http://easings.net/#easeInSine""" + return 1 - cos((t * pi / 2)) + + +def _ease_out_sine(t: float) -> float: + """http://easings.net/#easeOutSine""" + return sin((t * pi) / 2) + + +def _ease_sine(t: float) -> float: + """http://easings.net/#easeInOutSine""" + return -(cos(t * pi) - 1) / 2 + + +def _ease_in_quad(t: float) -> float: + """http://easings.net/#easeInQuad""" + return t * t + + +def _ease_out_quad(t: float) -> float: + """http://easings.net/#easeOutQuad""" + return 1 - (1 - t) * (1 - t) + + +def _ease_quad(t: float) -> float: + """http://easings.net/#easeInOutQuad""" + if t < 0.5: + return 2 * t * t + else: + return 1 - pow(-2 * t + 2, 2) / 2 + + +def _ease_in_cubic(t: float) -> float: + """http://easings.net/#easeInCubic""" + return t * t * t + + +def _ease_out_cubic(t: float) -> float: + """http://easings.net/#easeOutCubic""" + return 1 - pow(1 - t, 3) + + +def _ease_cubic(t: float) -> float: + """http://easings.net/#easeInOutCubic""" + if t < 0.5: + return 4 * t * t * t + else: + return 1 - pow(-2 * t + 2, 3) / 2 + + +def _ease_in_quart(t: float) -> float: + """http://easings.net/#easeInQuart""" + return t * t * t * t + + +def _ease_out_quart(t: float) -> float: + """http://easings.net/#easeOutQuart""" + return 1 - pow(1 - t, 4) + + +def _ease_quart(t: float) -> float: + """http://easings.net/#easeInOutQuart""" + if t < 0.5: + return 8 * t * t * t * t + else: + return 1 - pow(-2 * t + 2, 4) / 2 + + +def _ease_in_quint(t: float) -> float: + """http://easings.net/#easeInQint""" + return t * t * t * t * t + + +def _ease_out_quint(t: float) -> float: + """http://easings.net/#easeOutQint""" + return 1 - pow(1 - t, 5) + + +def _ease_quint(t: float) -> float: + """http://easings.net/#easeInOutQint""" + if t < 0.5: + return 16 * t * t * t * t * t + else: + return 1 - pow(-2 * t + 2, 5) / 2 + + +def _ease_in_expo(t: float) -> float: + """http://easings.net/#easeInExpo""" + if t == 0: + return 0 + return pow(2, 10 * t - 10) + + +def _ease_out_expo(t: float) -> float: + """http://easings.net/#easeOutExpo""" + if t == 1: + return 1 + return 1 - pow(2, -10 * t) + + +def _ease_expo(t: float) -> float: + """http://easings.net/#easeInOutExpo""" + if t == 0 or t == 1: + return t + elif t < 0.5: + return pow(2, 20 * t - 10) / 2 + else: + return (2 - pow(2, -20 * t + 10)) / 2 + + +def _ease_in_circ(t: float) -> float: + """http://easings.net/#easeInCirc""" + return 1 - sqrt(1 - pow(t, 2)) + + +def _ease_out_circ(t: float) -> float: + """http://easings.net/#easeOutCirc""" + return sqrt(1 - pow(t - 1, 2)) + + +def _ease_circ(t: float) -> float: + """http://easings.net/#easeInOutCirc""" + if t < 0.5: + return (1 - sqrt(1 - pow(2 * t, 2))) / 2 + else: + return (sqrt(1 - pow(-2 * t + 2, 2)) + 1) / 2 + + +def _ease_in_back(t: float) -> float: + """http://easings.net/#easeInBack""" + return (C3 * t * t * t) - (C1 * t * t) + + +def _ease_out_back(t: float) -> float: + """http://easings.net/#easeOutBack""" + return 1 + C3 + pow(t - 1, 3) + C1 * pow(t - 1, 2) + + +def _ease_back(t: float) -> float: + """http://easings.net/#easeInOutBack""" + if t < 0.5: + return (pow(2 * t, 2) * ((C2 + 1) * 2 * t - C2)) / 2 + else: + return (pow(2 * t - 2, 2) * ((C2 + 1) * (t * 2 - 2) + C2) + 2) / 2 + + +def _ease_in_elastic(t: float) -> float: + """http://easings.net/#easeInElastic""" + if t == 0 or t == 1: + return t + return -pow(2, 10 * t - 10) * sin((t * 10 - 10.75) * TAU_ON_THREE) + + +def _ease_out_elastic(t: float) -> float: + """http://easings.net/#easeOutElastic""" + if t == 0 or t == 1: + return t + return pow(2, -10 * t) * sin((t * 10 - 0.75) * TAU_ON_THREE) + 1 + + +def _ease_elastic(t: float) -> float: + """http://easings.net/#easeInOutElastic""" + if t == 0 or t == 1: + return t + if t < 0.5: + return -(pow(2, 20 * t - 10) * sin((20 * t - 11.125) * TAU_ON_FOUR_AND_A_HALF)) / 2 + else: + return (pow(2, -20 * t + 10) * sin((20 * t - 11.125) * TAU_ON_FOUR_AND_A_HALF)) / 2 + 1 + + +def _ease_in_bounce(t: float) -> float: + """http://easings.net/#easeInBounce""" + return 1 - (_ease_out_bounce(1 - t)) + + +def _ease_out_bounce(t: float) -> float: + """http://easings.net/#easeOutBounce""" + if t < 1 / D1: + return N1 * t * t + elif t < 2 / D1: + return N1 * ((t - 1.5) / D1) * (t - 1.5) + 0.75 + elif t < 2.5 / D1: + return N1 * ((t - 2.25) / D1) * (t - 2.25) + 0.9375 + else: + return N1 * ((t - 2.625) / D1) * (t - 2.625) + 0.984375 + + +def _ease_bounce(t: float) -> float: + """http://easings.net/#easeInOutBounce""" + if t < 0.5: + return (1 - _ease_out_bounce(1 - 2 * t)) / 2 + else: + return (1 + _ease_out_bounce(2 * t - 1)) / 2 + + +class Easing: + """:py:class:`.EasingFunction`s meant for passing into :py:meth:`.ease`.""" + + LINEAR = staticmethod(_ease_linear) + SINE = staticmethod(_ease_sine) + SINE_IN = staticmethod(_ease_in_sine) + SINE_OUT = staticmethod(_ease_out_sine) + QUAD = staticmethod(_ease_quad) + QUAD_IN = staticmethod(_ease_in_quad) + QUAD_OUT = staticmethod(_ease_out_quad) + CUBIC = staticmethod(_ease_cubic) + CUBIC_IN = staticmethod(_ease_in_cubic) + CUBIC_OUT = staticmethod(_ease_out_cubic) + QUART = staticmethod(_ease_quart) + QUART_IN = staticmethod(_ease_in_quart) + QUART_OUT = staticmethod(_ease_out_quart) + QUINT = staticmethod(_ease_quint) + QUINT_IN = staticmethod(_ease_in_quint) + QUINT_OUT = staticmethod(_ease_out_quint) + EXPO = staticmethod(_ease_expo) + EXPO_IN = staticmethod(_ease_in_expo) + EXPO_OUT = staticmethod(_ease_out_expo) + CIRC = staticmethod(_ease_circ) + CIRC_IN = staticmethod(_ease_in_circ) + CIRC_OUT = staticmethod(_ease_out_circ) + BACK = staticmethod(_ease_back) + BACK_IN = staticmethod(_ease_in_back) + BACK_OUT = staticmethod(_ease_out_back) + ELASTIC = staticmethod(_ease_elastic) + ELASTIC_IN = staticmethod(_ease_in_elastic) + ELASTIC_OUT = staticmethod(_ease_out_elastic) + BOUNCE = staticmethod(_ease_bounce) + BOUNCE_IN = staticmethod(_ease_in_bounce) + BOUNCE_OUT = staticmethod(_ease_out_bounce) + # Aliases to match easing.net names + SINE_IN_OUT = staticmethod(_ease_sine) + QUAD_IN_OUT = staticmethod(_ease_quad) + CUBIC_IN_OUT = staticmethod(_ease_cubic) + QUART_IN_OUT = staticmethod(_ease_quart) + QUINT_IN_OUT = staticmethod(_ease_quint) + EXPO_IN_OUT = staticmethod(_ease_expo) + CIRC_IN_OUT = staticmethod(_ease_circ) + BACK_IN_OUT = staticmethod(_ease_back) + ELASTIC_IN_OUT = staticmethod(_ease_elastic) + BOUNCE_IN_OUT = staticmethod(_ease_bounce) + + +# === END EASING FUNCTIONS === + + +def _clamp(x: float, low: float, high: float) -> float: + return high if x > high else max(x, low) + + +def perc(x: float, start: float, end: float) -> float: + """ + Convert a value ``x`` to be a percentage of progression between + ``start`` and ``end``. + """ + return (x - start) / (end - start) + + +def lerp(x: float, minimum: A, maximum: A) -> A: + """ + Convert a percentage ``x`` to be the value when progressed + that amount between ``minimum`` and ``maximum``. + """ + return minimum + ((maximum - minimum) * x) + + +EasingFunction = Callable[[float], float] + + +def ease( + minimum: A, + maximum: A, + start: float, + end: float, + t: float, + func: EasingFunction = Easing.LINEAR, + clamped: bool = True, +) -> A: + """Ease a value according to a curve. Useful for animating properties over time. + + Args: + minimum: any math-like object (a position, scale, value...); the "start position." + maximum: any math-like object (a position, scale, value...); the "end position." + start: a :py:class:`float` defining where progression begins, the "start time." + end: a :py:class:`float` defining where progression ends, the "end time." + t: a :py:class:`float` defining the current progression, the "current time." + func: a :py:class:`.EasingFunction` to modify the result with, typically an + attribute of :py:class:`.Easing`. Defaults to :py:attr:`.Easing.LINEAR`. + clamped: a :py:class:`bool`; whether or not to allow the animation to continue past + the ``start`` and ``end`` "times". Defaults to ``True``. + """ + p = perc(t, start, end) + if clamped: + p = _clamp(p, 0.0, 1.0) + new_p = func(p) + return lerp(new_p, minimum, maximum) From e8f85dc62a894f90517ab1dd5efdaea52efea48e Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Thu, 18 Dec 2025 10:51:27 -0500 Subject: [PATCH 02/15] deprecate easing.py by deleting it it doesn't look like it's used anywhere internally --- arcade/easing.py | 275 ----------------------------------------------- 1 file changed, 275 deletions(-) delete mode 100644 arcade/easing.py diff --git a/arcade/easing.py b/arcade/easing.py deleted file mode 100644 index aa2a2ddf5..000000000 --- a/arcade/easing.py +++ /dev/null @@ -1,275 +0,0 @@ -""" -Functions used to support easing -""" - -from collections.abc import Callable -from dataclasses import dataclass -from math import cos, pi, sin - -from .math import get_distance - - -@dataclass -class EasingData: - """ - Data class for holding information about easing. - """ - - start_period: float - cur_period: float - end_period: float - start_value: float - end_value: float - ease_function: Callable - - def reset(self) -> None: - """ - Reset the easing data to its initial state. - """ - self.cur_period = self.start_period - - -def linear(percent: float) -> float: - """ - Function for linear easing. - """ - return percent - - -def _flip(percent: float) -> float: - return 1.0 - percent - - -def smoothstep(percent: float) -> float: - """ - Function for smoothstep easing. - """ - return percent**2 * (3.0 - 2.0 * percent) - - -def ease_in(percent: float) -> float: - """ - Function for quadratic ease-in easing. - """ - return percent**2 - - -def ease_out(percent: float) -> float: - """ - Function for quadratic ease-out easing. - """ - return _flip(_flip(percent) * _flip(percent)) - - -def ease_in_out(percent: float) -> float: - """ - Function for quadratic easing in and out. - """ - - return 2 * percent**2 if percent < 0.5 else 1 - (-2 * percent + 2) ** 2 / 2 - - -def ease_out_elastic(percent: float) -> float: - """ - Function for elastic ease-out easing. - """ - c4 = 2 * pi / 3 - result = 0.0 - if percent == 1: - result = 1 - elif percent > 0: - result = (2 ** (-10 * percent)) * sin((percent * 10 - 0.75) * c4) + 1 - return result - - -def ease_out_bounce(percent: float) -> float: - """ - Function for a bouncy ease-out easing. - """ - n1 = 7.5625 - d1 = 2.75 - - if percent < 1 / d1: - return n1 * percent * percent - elif percent < 2 / d1: - percent_modified = percent - 1.5 / d1 - return n1 * percent_modified * percent_modified + 0.75 - elif percent < 2.5 / d1: - percent_modified = percent - 2.25 / d1 - return n1 * percent_modified * percent_modified + 0.9375 - else: - percent_modified = percent - 2.625 / d1 - return n1 * percent_modified * percent_modified + 0.984375 - - -def ease_in_back(percent: float) -> float: - """ - Function for ease_in easing which moves back before moving forward. - """ - c1 = 1.70158 - c3 = c1 + 1 - - return c3 * percent * percent * percent - c1 * percent * percent - - -def ease_out_back(percent: float) -> float: - """ - Function for ease_out easing which moves back before moving forward. - """ - c1 = 1.70158 - c3 = c1 + 1 - - return 1 + c3 * pow(percent - 1, 3) + c1 * pow(percent - 1, 2) - - -def ease_in_sin(percent: float) -> float: - """ - Function for ease_in easing using a sin wave - """ - return 1 - cos((percent * pi) / 2) - - -def ease_out_sin(percent: float) -> float: - """ - Function for ease_out easing using a sin wave - """ - return sin((percent * pi) / 2) - - -def ease_in_out_sin(percent: float) -> float: - """ - Function for easing in and out using a sin wave - """ - return -cos(percent * pi) * 0.5 + 0.5 - - -def easing(percent: float, easing_data: EasingData) -> float: - """ - Function for calculating return value for easing, given percent and easing data. - """ - return easing_data.start_value + ( - easing_data.end_value - easing_data.start_value - ) * easing_data.ease_function(percent) - - -def ease_angle( - start_angle: float, - end_angle: float, - *, - time=None, - rate=None, - ease_function: Callable = linear, -) -> EasingData | None: - """ - Set up easing for angles. - """ - while start_angle - end_angle > 180: - end_angle += 360 - - while start_angle - end_angle < -180: - end_angle -= 360 - - diff = abs(start_angle - end_angle) - if diff == 0: - return None - - if rate is not None: - time = diff / rate - - if time is None: - raise ValueError("Either the 'time' or the 'rate' parameter needs to be set.") - - easing_data = EasingData( - start_value=start_angle, - end_value=end_angle, - start_period=0, - cur_period=0, - end_period=time, - ease_function=ease_function, - ) - return easing_data - - -def ease_angle_update(easing_data: EasingData, delta_time: float) -> tuple[bool, float]: - """ - Update angle easing. - """ - done = False - easing_data.cur_period += delta_time - easing_data.cur_period = min(easing_data.cur_period, easing_data.end_period) - percent = easing_data.cur_period / easing_data.end_period - - angle = easing(percent, easing_data) - - if percent >= 1.0: - done = True - - while angle > 360: - angle -= 360 - - while angle < 0: - angle += 360 - - return done, angle - - -def ease_value( - start_value: float, end_value: float, *, time=None, rate=None, ease_function=linear -) -> EasingData: - """ - Get an easing value - """ - if rate is not None: - diff = abs(start_value - end_value) - time = diff / rate - - if time is None: - raise ValueError("Either the 'time' or the 'rate' parameter needs to be set.") - - easing_data = EasingData( - start_value=start_value, - end_value=end_value, - start_period=0, - cur_period=0, - end_period=time, - ease_function=ease_function, - ) - return easing_data - - -def ease_position( - start_position, end_position, *, time=None, rate=None, ease_function=linear -) -> tuple[EasingData, EasingData]: - """ - Get an easing position - """ - distance = get_distance(start_position[0], start_position[1], end_position[0], end_position[1]) - - if rate is not None: - time = distance / rate - - easing_data_x = ease_value( - start_position[0], end_position[0], time=time, ease_function=ease_function - ) - easing_data_y = ease_value( - start_position[1], end_position[1], time=time, ease_function=ease_function - ) - - return easing_data_x, easing_data_y - - -def ease_update(easing_data: EasingData, delta_time: float) -> tuple[bool, float]: - """ - Update easing between two values/ - """ - easing_data.cur_period += delta_time - easing_data.cur_period = min(easing_data.cur_period, easing_data.end_period) - if easing_data.end_period == 0: - percent = 1.0 - value = easing_data.end_value - else: - percent = easing_data.cur_period / easing_data.end_period - value = easing(percent, easing_data) - - done = percent >= 1.0 - return done, value From 2996316cd2ed1f6c82b827c2a834d1dec3281b01 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Thu, 18 Dec 2025 13:06:44 -0500 Subject: [PATCH 03/15] explain constants --- arcade/anim/easing.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/arcade/anim/easing.py b/arcade/anim/easing.py index ecdfe7231..6f6c9bef8 100644 --- a/arcade/anim/easing.py +++ b/arcade/anim/easing.py @@ -17,9 +17,12 @@ def __sub__(self: T, other: T | float, /) -> T: ... # === BEGIN EASING FUNCTIONS === -C1 = 1.70158 -C2 = C1 * 1.525 -C3 = C1 + 1 +# CONSTANTS USED FOR EASING EQUATIONS +# *: The constants C2, C3, N1, and D1 don't have clean analogies, +# so remain unnamed. +TEN_PERCENT_BOUNCE = 1.70158 +C2 = TEN_PERCENT_BOUNCE * 1.525 +C3 = TEN_PERCENT_BOUNCE + 1 TAU_ON_THREE = tau / 3 TAU_ON_FOUR_AND_A_HALF = tau / 4.5 N1 = 7.5625 @@ -161,12 +164,12 @@ def _ease_circ(t: float) -> float: def _ease_in_back(t: float) -> float: """http://easings.net/#easeInBack""" - return (C3 * t * t * t) - (C1 * t * t) + return (C3 * t * t * t) - (TEN_PERCENT_BOUNCE * t * t) def _ease_out_back(t: float) -> float: """http://easings.net/#easeOutBack""" - return 1 + C3 + pow(t - 1, 3) + C1 * pow(t - 1, 2) + return 1 + C3 + pow(t - 1, 3) + TEN_PERCENT_BOUNCE * pow(t - 1, 2) def _ease_back(t: float) -> float: From bfc01c2e92d591a32e94f3e506710059371e5ccb Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Thu, 18 Dec 2025 19:23:11 -0500 Subject: [PATCH 04/15] remove obsolete examples --- arcade/examples/easing_example_1.py | 209 ---------------------------- arcade/examples/easing_example_2.py | 178 ----------------------- 2 files changed, 387 deletions(-) delete mode 100644 arcade/examples/easing_example_1.py delete mode 100644 arcade/examples/easing_example_2.py diff --git a/arcade/examples/easing_example_1.py b/arcade/examples/easing_example_1.py deleted file mode 100644 index 032d1d102..000000000 --- a/arcade/examples/easing_example_1.py +++ /dev/null @@ -1,209 +0,0 @@ -""" -Example showing how to use the easing functions for position. - -See: -https://easings.net/ -...for a great guide on the theory behind how easings can work. - -See example 2 for how to use easings for angles. - -If Python and Arcade are installed, this example can be run from the command line with: -python -m arcade.examples.easing_example_1 -""" - -import arcade -from arcade import easing -from arcade.types import Color - -SPRITE_SCALING = 0.5 - -WINDOW_WIDTH = 1280 -WINDOW_HEIGHT = 720 -WINDOW_TITLE = "Easing Example" - -BACKGROUND_COLOR = "#F5D167" -TEXT_COLOR = "#4B1DF2" -BALL_COLOR = "#42B5EB" -LINE_COLOR = "#45E6D0" -LINE_WIDTH = 3 - -X_START = 40 -X_END = 1200 -Y_INTERVAL = 60 -BALL_RADIUS = 13 -TIME = 3.0 - - -class EasingCircle(arcade.SpriteCircle): - """Player class""" - - def __init__(self, radius, color, center_x: float = 0, center_y: float = 0): - """Set up the player""" - - # Call the parent init - super().__init__(radius, color, center_x=center_x, center_y=center_y) - - self.easing_x_data = None - self.easing_y_data = None - - def update(self, delta_time: float = 1 / 60): - if self.easing_x_data is not None: - done, self.center_x = easing.ease_update(self.easing_x_data, delta_time) - if done: - x = X_START - if self.center_x < WINDOW_WIDTH / 2: - x = X_END - ex, ey = easing.ease_position( - self.position, - (x, self.center_y), - rate=180, - ease_function=self.easing_x_data.ease_function, - ) - self.easing_x_data = ex - - if self.easing_y_data is not None: - done, self.center_y = easing.ease_update(self.easing_y_data, delta_time) - if done: - self.easing_y_data = None - - -class GameView(arcade.View): - """Main application class.""" - - def __init__(self): - """Initializer""" - - # Call the parent class initializer - super().__init__() - - # Set the background color - self.background_color = Color.from_hex_string(BACKGROUND_COLOR) - - self.ball_list = None - self.text_list = [] - self.lines = None - - def setup(self): - """Set up the game and initialize the variables.""" - - # Sprite lists - self.ball_list = arcade.SpriteList() - self.lines = arcade.shape_list.ShapeElementList() - color = Color.from_hex_string(BALL_COLOR) - shared_ball_kwargs = dict(radius=BALL_RADIUS, color=color) - - def create_ball(ball_y, ease_function): - ball = EasingCircle(**shared_ball_kwargs, center_x=X_START, center_y=ball_y) - p1 = ball.position - p2 = (X_END, ball_y) - ex, ey = easing.ease_position(p1, p2, time=TIME, ease_function=ease_function) - ball.ease_function = ease_function - ball.easing_x_data = ex - ball.easing_y_data = ey - return ball - - def create_line(line_y): - line = arcade.shape_list.create_line( - X_START, - line_y - BALL_RADIUS - LINE_WIDTH, - X_END, - line_y - BALL_RADIUS, - line_color, - line_width=LINE_WIDTH, - ) - return line - - def create_text(text_string): - text = arcade.Text( - text_string, - x=X_START, - y=y - BALL_RADIUS, - color=text_color, - font_size=24, - ) - return text - - def add_item(item_y, ease_function, text): - ball = create_ball(item_y, ease_function) - self.ball_list.append(ball) - text = create_text(text) - self.text_list.append(text) - line = create_line(item_y) - self.lines.append(line) - - text_color = Color.from_hex_string(TEXT_COLOR) - line_color = Color.from_hex_string(LINE_COLOR) - - y = Y_INTERVAL - add_item(y, easing.linear, "Linear") - - y += Y_INTERVAL - add_item(y, easing.ease_out, "Ease out") - - y += Y_INTERVAL - add_item(y, easing.ease_in, "Ease in") - - y += Y_INTERVAL - add_item(y, easing.smoothstep, "Smoothstep") - - y += Y_INTERVAL - add_item(y, easing.ease_in_out, "Ease in/out") - - y += Y_INTERVAL - add_item(y, easing.ease_out_elastic, "Ease out elastic") - - y += Y_INTERVAL - add_item(y, easing.ease_in_back, "Ease in back") - - y += Y_INTERVAL - add_item(y, easing.ease_out_back, "Ease out back") - - y += Y_INTERVAL - add_item(y, easing.ease_in_sin, "Ease in sin") - - y += Y_INTERVAL - add_item(y, easing.ease_out_sin, "Ease out sin") - - y += Y_INTERVAL - add_item(y, easing.ease_in_out_sin, "Ease in out sin") - - def on_draw(self): - """Render the screen.""" - - # This command has to happen before we start drawing - self.clear() - - self.lines.draw() - - # Draw all the sprites. - self.ball_list.draw() - - for text in self.text_list: - text.draw() - - def on_update(self, delta_time): - """Movement and game logic""" - - # Call update on all sprites (The sprites don't do much in this - # example though.) - self.ball_list.update(delta_time) - - -def main(): - """Main function""" - # Create a window class. This is what actually shows up on screen - window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE) - - # Create and setup the GameView - game = GameView() - game.setup() - - # Show GameView on screen - window.show_view(game) - - # Start the arcade game loop - arcade.run() - - -if __name__ == "__main__": - main() diff --git a/arcade/examples/easing_example_2.py b/arcade/examples/easing_example_2.py deleted file mode 100644 index 1467b71e1..000000000 --- a/arcade/examples/easing_example_2.py +++ /dev/null @@ -1,178 +0,0 @@ -""" -Example showing how to use the easing functions for position. -Example showing how to use easing for angles. - -See: -https://easings.net/ -...for a great guide on the theory behind how easings can work. - -If Python and Arcade are installed, this example can be run from the command line with: -python -m arcade.examples.easing_example_2 -""" - -import arcade -from arcade import easing - -SPRITE_SCALING = 1.0 - -WINDOW_WIDTH = 1280 -WINDOW_HEIGHT = 720 -WINDOW_TITLE = "Easing Example" - - -class Player(arcade.Sprite): - """Player class""" - - def __init__(self, image, scale): - """Set up the player""" - - # Call the parent init - super().__init__(image, scale=scale) - - self.easing_angle_data = None - self.easing_x_data = None - self.easing_y_data = None - - def update(self, delta_time: float = 1 / 60): - if self.easing_angle_data is not None: - done, self.angle = easing.ease_angle_update(self.easing_angle_data, delta_time) - if done: - self.easing_angle_data = None - - if self.easing_x_data is not None: - done, self.center_x = easing.ease_update(self.easing_x_data, delta_time) - if done: - self.easing_x_data = None - - if self.easing_y_data is not None: - done, self.center_y = easing.ease_update(self.easing_y_data, delta_time) - if done: - self.easing_y_data = None - - -class GameView(arcade.View): - """Main application class.""" - - def __init__(self): - """Initializer""" - - # Call the parent class initializer - super().__init__() - - # Set up the player info - self.player_list = arcade.SpriteList() - - # Load the player texture. The ship points up by default. We need it to point right. - # That's why we rotate it 90 degrees clockwise. - texture = arcade.load_texture(":resources:images/space_shooter/playerShip1_orange.png") - texture = texture.rotate_90() - - # Set up the player - self.player_sprite = Player(texture, SPRITE_SCALING) - self.player_sprite.angle = 0 - self.player_sprite.center_x = WINDOW_WIDTH / 2 - self.player_sprite.center_y = WINDOW_HEIGHT / 2 - self.player_list.append(self.player_sprite) - - # Set the background color - self.background_color = arcade.color.BLACK - self.text = "Move the mouse and press 1-9 to apply an easing function." - - def on_draw(self): - """Render the screen.""" - - # This command has to happen before we start drawing - self.clear() - - # Draw all the sprites. - self.player_list.draw() - - arcade.draw_text(self.text, 15, 15, arcade.color.WHITE, 24) - - def on_update(self, delta_time): - """Movement and game logic""" - - # Call update on all sprites (The sprites don't do much in this - # example though.) - self.player_list.update(delta_time) - - def on_key_press(self, key, modifiers): - x = self.window.mouse["x"] - y = self.window.mouse["y"] - - if key == arcade.key.KEY_1: - angle = arcade.math.get_angle_degrees( - x1=self.player_sprite.position[0], y1=self.player_sprite.position[1], x2=x, y2=y - ) - self.player_sprite.angle = angle - self.text = "Instant angle change" - if key in [arcade.key.KEY_2, arcade.key.KEY_3, arcade.key.KEY_4, arcade.key.KEY_5]: - p1 = self.player_sprite.position - p2 = (x, y) - end_angle = arcade.math.get_angle_degrees(p1[0], p1[1], p2[0], p2[1]) - start_angle = self.player_sprite.angle - if key == arcade.key.KEY_2: - ease_function = easing.linear - self.text = "Linear easing - angle" - elif key == arcade.key.KEY_3: - ease_function = easing.ease_in - self.text = "Ease in - angle" - elif key == arcade.key.KEY_4: - ease_function = easing.ease_out - self.text = "Ease out - angle" - elif key == arcade.key.KEY_5: - ease_function = easing.smoothstep - self.text = "Smoothstep - angle" - else: - raise ValueError("?") - - self.player_sprite.easing_angle_data = easing.ease_angle( - start_angle, end_angle, rate=180, ease_function=ease_function - ) - - if key in [arcade.key.KEY_6, arcade.key.KEY_7, arcade.key.KEY_8, arcade.key.KEY_9]: - p1 = self.player_sprite.position - p2 = (x, y) - if key == arcade.key.KEY_6: - ease_function = easing.linear - self.text = "Linear easing - position" - elif key == arcade.key.KEY_7: - ease_function = easing.ease_in - self.text = "Ease in - position" - elif key == arcade.key.KEY_8: - ease_function = easing.ease_out - self.text = "Ease out - position" - elif key == arcade.key.KEY_9: - ease_function = easing.smoothstep - self.text = "Smoothstep - position" - else: - raise ValueError("?") - - ex, ey = easing.ease_position(p1, p2, rate=180, ease_function=ease_function) - self.player_sprite.easing_x_data = ex - self.player_sprite.easing_y_data = ey - - def on_mouse_press(self, x: float, y: float, button: int, modifiers: int): - angle = arcade.math.get_angle_degrees( - x1=self.player_sprite.position[0], y1=self.player_sprite.position[1], x2=x, y2=y - ) - self.player_sprite.angle = angle - - -def main(): - """ Main function """ - # Create a window class. This is what actually shows up on screen - window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE) - - # Create the GameView - game = GameView() - - # Show GameView on screen - window.show_view(game) - - # Start the arcade game loop - arcade.run() - - -if __name__ == "__main__": - main() From 678a6f9fee3125804d3e05b243c37d7b6b7635a8 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Dec 2025 21:14:37 -0500 Subject: [PATCH 05/15] Get doc building --- doc/api_docs/arcade.rst | 2 +- doc/example_code/easing_example_1.rst | 17 ----------------- doc/example_code/easing_example_2.rst | 17 ----------------- doc/example_code/index.rst | 11 ++--------- util/update_quick_index.py | 2 +- 5 files changed, 4 insertions(+), 45 deletions(-) delete mode 100644 doc/example_code/easing_example_1.rst delete mode 100644 doc/example_code/easing_example_2.rst diff --git a/doc/api_docs/arcade.rst b/doc/api_docs/arcade.rst index a541e0500..c259a54b1 100644 --- a/doc/api_docs/arcade.rst +++ b/doc/api_docs/arcade.rst @@ -40,7 +40,7 @@ for the Python Arcade library. See also: api/path_finding api/isometric api/earclip - api/easing + api/anim api/open_gl api/math gl/index diff --git a/doc/example_code/easing_example_1.rst b/doc/example_code/easing_example_1.rst deleted file mode 100644 index aa74129a4..000000000 --- a/doc/example_code/easing_example_1.rst +++ /dev/null @@ -1,17 +0,0 @@ -:orphan: - -.. _easing_example_1: - -Easing Example 1 -================ - -.. image:: images/easing_example_1.png - :width: 600px - :align: center - :alt: Easing Example - -Source ------- -.. literalinclude:: ../../arcade/examples/easing_example_1.py - :caption: easing_example.py - :linenos: diff --git a/doc/example_code/easing_example_2.rst b/doc/example_code/easing_example_2.rst deleted file mode 100644 index 76e2f582c..000000000 --- a/doc/example_code/easing_example_2.rst +++ /dev/null @@ -1,17 +0,0 @@ -:orphan: - -.. _easing_example_2: - -Easing Example 2 -================ - -.. image:: images/easing_example_2.png - :width: 600px - :align: center - :alt: Easing Example - -Source ------- -.. literalinclude:: ../../arcade/examples/easing_example_2.py - :caption: easing_example.py - :linenos: diff --git a/doc/example_code/index.rst b/doc/example_code/index.rst index 5d5359e8e..ccb6508da 100644 --- a/doc/example_code/index.rst +++ b/doc/example_code/index.rst @@ -228,17 +228,10 @@ Non-Player Movement Easing ^^^^^^ -.. figure:: images/thumbs/easing_example_1.png - :figwidth: 170px - :target: easing_example_1.html - - :ref:`easing_example_1` +.. note:: Easing is a work in progress refactor. -.. figure:: images/thumbs/easing_example_2.png - :figwidth: 170px - :target: easing_example_2.html + Please see :py:mod:`arcade.anim`. - :ref:`easing_example_2` Calculating a Path ^^^^^^^^^^^^^^^^^^ diff --git a/util/update_quick_index.py b/util/update_quick_index.py index e6729b1af..0b050fc88 100644 --- a/util/update_quick_index.py +++ b/util/update_quick_index.py @@ -188,7 +188,7 @@ "title": "Isometric Map (incomplete)", "use_declarations_in": ["arcade.isometric"], }, - "easing.rst": {"title": "Easing", "use_declarations_in": ["arcade.easing"]}, + "anim.rst": {"title": "Easing", "use_declarations_in": ["arcade.anim"]}, "utility.rst": { "title": "Misc Utility Functions", "use_declarations_in": ["arcade", "arcade.__main__", "arcade.utils"], From 061d424f9abe9a7380a521ad66a654d7e0de5385 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Sat, 20 Dec 2025 03:18:36 -0500 Subject: [PATCH 06/15] minor improvements --- arcade/anim/easing.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/arcade/anim/easing.py b/arcade/anim/easing.py index 6f6c9bef8..686bc023a 100644 --- a/arcade/anim/easing.py +++ b/arcade/anim/easing.py @@ -231,7 +231,9 @@ def _ease_bounce(t: float) -> float: class Easing: """:py:class:`.EasingFunction`s meant for passing into :py:meth:`.ease`.""" - + # This is a bucket of staticmethods because typing. + # Enum hates this, and they can't be classmethods. + # Sorry that this looks strange! -- DigiDuncan LINEAR = staticmethod(_ease_linear) SINE = staticmethod(_ease_sine) SINE_IN = staticmethod(_ease_in_sine) @@ -264,16 +266,16 @@ class Easing: BOUNCE_IN = staticmethod(_ease_in_bounce) BOUNCE_OUT = staticmethod(_ease_out_bounce) # Aliases to match easing.net names - SINE_IN_OUT = staticmethod(_ease_sine) - QUAD_IN_OUT = staticmethod(_ease_quad) - CUBIC_IN_OUT = staticmethod(_ease_cubic) - QUART_IN_OUT = staticmethod(_ease_quart) - QUINT_IN_OUT = staticmethod(_ease_quint) - EXPO_IN_OUT = staticmethod(_ease_expo) - CIRC_IN_OUT = staticmethod(_ease_circ) - BACK_IN_OUT = staticmethod(_ease_back) - ELASTIC_IN_OUT = staticmethod(_ease_elastic) - BOUNCE_IN_OUT = staticmethod(_ease_bounce) + SINE_IN_OUT = SINE + QUAD_IN_OUT = QUAD + CUBIC_IN_OUT = CUBIC + QUART_IN_OUT = QUART + QUINT_IN_OUT = QUINT + EXPO_IN_OUT = EXPO + CIRC_IN_OUT = CIRC + BACK_IN_OUT = BACK + ELASTIC_IN_OUT = ELASTIC + BOUNCE_IN_OUT = BOUNCE # === END EASING FUNCTIONS === From 4c1126d0cbd940db549e47dd1a41ede8c67034a5 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 24 Dec 2025 03:51:31 -0500 Subject: [PATCH 07/15] just define the methods on the class --- arcade/anim/easing.py | 435 ++++++++++++++++++++---------------------- 1 file changed, 204 insertions(+), 231 deletions(-) diff --git a/arcade/anim/easing.py b/arcade/anim/easing.py index 686bc023a..8c175e2d2 100644 --- a/arcade/anim/easing.py +++ b/arcade/anim/easing.py @@ -29,242 +29,215 @@ def __sub__(self: T, other: T | float, /) -> T: ... D1 = 2.75 -def _ease_linear(t: float) -> float: - return t - - -def _ease_in_sine(t: float) -> float: - """http://easings.net/#easeInSine""" - return 1 - cos((t * pi / 2)) - - -def _ease_out_sine(t: float) -> float: - """http://easings.net/#easeOutSine""" - return sin((t * pi) / 2) - - -def _ease_sine(t: float) -> float: - """http://easings.net/#easeInOutSine""" - return -(cos(t * pi) - 1) / 2 - - -def _ease_in_quad(t: float) -> float: - """http://easings.net/#easeInQuad""" - return t * t - - -def _ease_out_quad(t: float) -> float: - """http://easings.net/#easeOutQuad""" - return 1 - (1 - t) * (1 - t) - - -def _ease_quad(t: float) -> float: - """http://easings.net/#easeInOutQuad""" - if t < 0.5: - return 2 * t * t - else: - return 1 - pow(-2 * t + 2, 2) / 2 - - -def _ease_in_cubic(t: float) -> float: - """http://easings.net/#easeInCubic""" - return t * t * t - - -def _ease_out_cubic(t: float) -> float: - """http://easings.net/#easeOutCubic""" - return 1 - pow(1 - t, 3) - - -def _ease_cubic(t: float) -> float: - """http://easings.net/#easeInOutCubic""" - if t < 0.5: - return 4 * t * t * t - else: - return 1 - pow(-2 * t + 2, 3) / 2 - - -def _ease_in_quart(t: float) -> float: - """http://easings.net/#easeInQuart""" - return t * t * t * t - - -def _ease_out_quart(t: float) -> float: - """http://easings.net/#easeOutQuart""" - return 1 - pow(1 - t, 4) - - -def _ease_quart(t: float) -> float: - """http://easings.net/#easeInOutQuart""" - if t < 0.5: - return 8 * t * t * t * t - else: - return 1 - pow(-2 * t + 2, 4) / 2 - - -def _ease_in_quint(t: float) -> float: - """http://easings.net/#easeInQint""" - return t * t * t * t * t - - -def _ease_out_quint(t: float) -> float: - """http://easings.net/#easeOutQint""" - return 1 - pow(1 - t, 5) - - -def _ease_quint(t: float) -> float: - """http://easings.net/#easeInOutQint""" - if t < 0.5: - return 16 * t * t * t * t * t - else: - return 1 - pow(-2 * t + 2, 5) / 2 - - -def _ease_in_expo(t: float) -> float: - """http://easings.net/#easeInExpo""" - if t == 0: - return 0 - return pow(2, 10 * t - 10) - - -def _ease_out_expo(t: float) -> float: - """http://easings.net/#easeOutExpo""" - if t == 1: - return 1 - return 1 - pow(2, -10 * t) - - -def _ease_expo(t: float) -> float: - """http://easings.net/#easeInOutExpo""" - if t == 0 or t == 1: - return t - elif t < 0.5: - return pow(2, 20 * t - 10) / 2 - else: - return (2 - pow(2, -20 * t + 10)) / 2 - - -def _ease_in_circ(t: float) -> float: - """http://easings.net/#easeInCirc""" - return 1 - sqrt(1 - pow(t, 2)) - - -def _ease_out_circ(t: float) -> float: - """http://easings.net/#easeOutCirc""" - return sqrt(1 - pow(t - 1, 2)) - - -def _ease_circ(t: float) -> float: - """http://easings.net/#easeInOutCirc""" - if t < 0.5: - return (1 - sqrt(1 - pow(2 * t, 2))) / 2 - else: - return (sqrt(1 - pow(-2 * t + 2, 2)) + 1) / 2 - - -def _ease_in_back(t: float) -> float: - """http://easings.net/#easeInBack""" - return (C3 * t * t * t) - (TEN_PERCENT_BOUNCE * t * t) - - -def _ease_out_back(t: float) -> float: - """http://easings.net/#easeOutBack""" - return 1 + C3 + pow(t - 1, 3) + TEN_PERCENT_BOUNCE * pow(t - 1, 2) - - -def _ease_back(t: float) -> float: - """http://easings.net/#easeInOutBack""" - if t < 0.5: - return (pow(2 * t, 2) * ((C2 + 1) * 2 * t - C2)) / 2 - else: - return (pow(2 * t - 2, 2) * ((C2 + 1) * (t * 2 - 2) + C2) + 2) / 2 - - -def _ease_in_elastic(t: float) -> float: - """http://easings.net/#easeInElastic""" - if t == 0 or t == 1: - return t - return -pow(2, 10 * t - 10) * sin((t * 10 - 10.75) * TAU_ON_THREE) - - -def _ease_out_elastic(t: float) -> float: - """http://easings.net/#easeOutElastic""" - if t == 0 or t == 1: - return t - return pow(2, -10 * t) * sin((t * 10 - 0.75) * TAU_ON_THREE) + 1 - - -def _ease_elastic(t: float) -> float: - """http://easings.net/#easeInOutElastic""" - if t == 0 or t == 1: - return t - if t < 0.5: - return -(pow(2, 20 * t - 10) * sin((20 * t - 11.125) * TAU_ON_FOUR_AND_A_HALF)) / 2 - else: - return (pow(2, -20 * t + 10) * sin((20 * t - 11.125) * TAU_ON_FOUR_AND_A_HALF)) / 2 + 1 - - -def _ease_in_bounce(t: float) -> float: - """http://easings.net/#easeInBounce""" - return 1 - (_ease_out_bounce(1 - t)) - - -def _ease_out_bounce(t: float) -> float: - """http://easings.net/#easeOutBounce""" - if t < 1 / D1: - return N1 * t * t - elif t < 2 / D1: - return N1 * ((t - 1.5) / D1) * (t - 1.5) + 0.75 - elif t < 2.5 / D1: - return N1 * ((t - 2.25) / D1) * (t - 2.25) + 0.9375 - else: - return N1 * ((t - 2.625) / D1) * (t - 2.625) + 0.984375 - - -def _ease_bounce(t: float) -> float: - """http://easings.net/#easeInOutBounce""" - if t < 0.5: - return (1 - _ease_out_bounce(1 - 2 * t)) / 2 - else: - return (1 + _ease_out_bounce(2 * t - 1)) / 2 - - class Easing: """:py:class:`.EasingFunction`s meant for passing into :py:meth:`.ease`.""" + # This is a bucket of staticmethods because typing. # Enum hates this, and they can't be classmethods. + # That's why their capitalized, it's meant to be an Enum-like # Sorry that this looks strange! -- DigiDuncan - LINEAR = staticmethod(_ease_linear) - SINE = staticmethod(_ease_sine) - SINE_IN = staticmethod(_ease_in_sine) - SINE_OUT = staticmethod(_ease_out_sine) - QUAD = staticmethod(_ease_quad) - QUAD_IN = staticmethod(_ease_in_quad) - QUAD_OUT = staticmethod(_ease_out_quad) - CUBIC = staticmethod(_ease_cubic) - CUBIC_IN = staticmethod(_ease_in_cubic) - CUBIC_OUT = staticmethod(_ease_out_cubic) - QUART = staticmethod(_ease_quart) - QUART_IN = staticmethod(_ease_in_quart) - QUART_OUT = staticmethod(_ease_out_quart) - QUINT = staticmethod(_ease_quint) - QUINT_IN = staticmethod(_ease_in_quint) - QUINT_OUT = staticmethod(_ease_out_quint) - EXPO = staticmethod(_ease_expo) - EXPO_IN = staticmethod(_ease_in_expo) - EXPO_OUT = staticmethod(_ease_out_expo) - CIRC = staticmethod(_ease_circ) - CIRC_IN = staticmethod(_ease_in_circ) - CIRC_OUT = staticmethod(_ease_out_circ) - BACK = staticmethod(_ease_back) - BACK_IN = staticmethod(_ease_in_back) - BACK_OUT = staticmethod(_ease_out_back) - ELASTIC = staticmethod(_ease_elastic) - ELASTIC_IN = staticmethod(_ease_in_elastic) - ELASTIC_OUT = staticmethod(_ease_out_elastic) - BOUNCE = staticmethod(_ease_bounce) - BOUNCE_IN = staticmethod(_ease_in_bounce) - BOUNCE_OUT = staticmethod(_ease_out_bounce) + + @staticmethod + def LINEAR(t: float) -> float: + """Essentially the 'null' case for easing. Does no easing.""" + return t + + @staticmethod + def SINE_IN(t: float) -> float: + """http://easings.net/#easeInSine""" + return 1 - cos((t * pi / 2)) + + @staticmethod + def SINE_OUT(t: float) -> float: + """http://easings.net/#easeOutSine""" + return sin((t * pi) / 2) + + @staticmethod + def SINE(t: float) -> float: + """http://easings.net/#easeInOutSine""" + return -(cos(t * pi) - 1) / 2 + + @staticmethod + def QUAD_IN(t: float) -> float: + """http://easings.net/#easeInQuad""" + return t * t + + @staticmethod + def QUAD_OUT(t: float) -> float: + """http://easings.net/#easeOutQuad""" + return 1 - (1 - t) * (1 - t) + + @staticmethod + def QUAD(t: float) -> float: + """http://easings.net/#easeInOutQuad""" + if t < 0.5: + return 2 * t * t + else: + return 1 - pow(-2 * t + 2, 2) / 2 + + @staticmethod + def CUBIC_IN(t: float) -> float: + """http://easings.net/#easeInCubic""" + return t * t * t + + @staticmethod + def CUBIC_OUT(t: float) -> float: + """http://easings.net/#easeOutCubic""" + return 1 - pow(1 - t, 3) + + @staticmethod + def CUBIC(t: float) -> float: + """http://easings.net/#easeInOutCubic""" + if t < 0.5: + return 4 * t * t * t + else: + return 1 - pow(-2 * t + 2, 3) / 2 + + @staticmethod + def QUART_IN(t: float) -> float: + """http://easings.net/#easeInQuart""" + return t * t * t * t + + @staticmethod + def QUART_OUT(t: float) -> float: + """http://easings.net/#easeOutQuart""" + return 1 - pow(1 - t, 4) + + @staticmethod + def QUART(t: float) -> float: + """http://easings.net/#easeInOutQuart""" + if t < 0.5: + return 8 * t * t * t * t + else: + return 1 - pow(-2 * t + 2, 4) / 2 + + @staticmethod + def QUINT_IN(t: float) -> float: + """http://easings.net/#easeInQint""" + return t * t * t * t * t + + @staticmethod + def QUINT_OUT(t: float) -> float: + """http://easings.net/#easeOutQint""" + return 1 - pow(1 - t, 5) + + @staticmethod + def QUINT(t: float) -> float: + """http://easings.net/#easeInOutQint""" + if t < 0.5: + return 16 * t * t * t * t * t + else: + return 1 - pow(-2 * t + 2, 5) / 2 + + @staticmethod + def EXPO_IN(t: float) -> float: + """http://easings.net/#easeInExpo""" + if t == 0: + return 0 + return pow(2, 10 * t - 10) + + @staticmethod + def EXPO_OUT(t: float) -> float: + """http://easings.net/#easeOutExpo""" + if t == 1: + return 1 + return 1 - pow(2, -10 * t) + + @staticmethod + def EXPO(t: float) -> float: + """http://easings.net/#easeInOutExpo""" + if t == 0 or t == 1: + return t + elif t < 0.5: + return pow(2, 20 * t - 10) / 2 + else: + return (2 - pow(2, -20 * t + 10)) / 2 + + @staticmethod + def CIRC_IN(t: float) -> float: + """http://easings.net/#easeInCirc""" + return 1 - sqrt(1 - pow(t, 2)) + + @staticmethod + def CIRC_OUT(t: float) -> float: + """http://easings.net/#easeOutCirc""" + return sqrt(1 - pow(t - 1, 2)) + + @staticmethod + def CIRC(t: float) -> float: + """http://easings.net/#easeInOutCirc""" + if t < 0.5: + return (1 - sqrt(1 - pow(2 * t, 2))) / 2 + else: + return (sqrt(1 - pow(-2 * t + 2, 2)) + 1) / 2 + + @staticmethod + def BACK_IN(t: float) -> float: + """http://easings.net/#easeInBack""" + return (C3 * t * t * t) - (TEN_PERCENT_BOUNCE * t * t) + + @staticmethod + def BACK_OUT(t: float) -> float: + """http://easings.net/#easeOutBack""" + return 1 + C3 + pow(t - 1, 3) + TEN_PERCENT_BOUNCE * pow(t - 1, 2) + + @staticmethod + def BACK(t: float) -> float: + """http://easings.net/#easeInOutBack""" + if t < 0.5: + return (pow(2 * t, 2) * ((C2 + 1) * 2 * t - C2)) / 2 + else: + return (pow(2 * t - 2, 2) * ((C2 + 1) * (t * 2 - 2) + C2) + 2) / 2 + + @staticmethod + def ELASTIC_IN(t: float) -> float: + """http://easings.net/#easeInElastic""" + if t == 0 or t == 1: + return t + return -pow(2, 10 * t - 10) * sin((t * 10 - 10.75) * TAU_ON_THREE) + + @staticmethod + def ELASTIC_OUT(t: float) -> float: + """http://easings.net/#easeOutElastic""" + if t == 0 or t == 1: + return t + return pow(2, -10 * t) * sin((t * 10 - 0.75) * TAU_ON_THREE) + 1 + + @staticmethod + def ELASTIC(t: float) -> float: + """http://easings.net/#easeInOutElastic""" + if t == 0 or t == 1: + return t + if t < 0.5: + return -(pow(2, 20 * t - 10) * sin((20 * t - 11.125) * TAU_ON_FOUR_AND_A_HALF)) / 2 + else: + return (pow(2, -20 * t + 10) * sin((20 * t - 11.125) * TAU_ON_FOUR_AND_A_HALF)) / 2 + 1 + + @staticmethod + def BOUNCE_IN(t: float) -> float: + """http://easings.net/#easeInBounce""" + return 1 - (Easing.BOUNCE_OUT(1 - t)) + + @staticmethod + def BOUNCE_OUT(t: float) -> float: + """http://easings.net/#easeOutBounce""" + if t < 1 / D1: + return N1 * t * t + elif t < 2 / D1: + return N1 * ((t - 1.5) / D1) * (t - 1.5) + 0.75 + elif t < 2.5 / D1: + return N1 * ((t - 2.25) / D1) * (t - 2.25) + 0.9375 + else: + return N1 * ((t - 2.625) / D1) * (t - 2.625) + 0.984375 + + @staticmethod + def BOUNCE(t: float) -> float: + """http://easings.net/#easeInOutBounce""" + if t < 0.5: + return (1 - Easing.BOUNCE_OUT(1 - 2 * t)) / 2 + else: + return (1 + Easing.BOUNCE_OUT(2 * t - 1)) / 2 + # Aliases to match easing.net names SINE_IN_OUT = SINE QUAD_IN_OUT = QUAD From b2dc125f5edf34875f42a87a1714102fd4a506d2 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 24 Dec 2025 04:15:04 -0500 Subject: [PATCH 08/15] Fix sphinx conf to render docstrings --- util/update_quick_index.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/util/update_quick_index.py b/util/update_quick_index.py index 0b050fc88..bcdee6572 100644 --- a/util/update_quick_index.py +++ b/util/update_quick_index.py @@ -188,7 +188,9 @@ "title": "Isometric Map (incomplete)", "use_declarations_in": ["arcade.isometric"], }, - "anim.rst": {"title": "Easing", "use_declarations_in": ["arcade.anim"]}, + "anim.rst": { + "title": "Easing", "use_declarations_in": ["arcade.anim.easing"] + }, "utility.rst": { "title": "Misc Utility Functions", "use_declarations_in": ["arcade", "arcade.__main__", "arcade.utils"], From ac69888986ee92626c15305021e696bc1cf18525 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 24 Dec 2025 05:20:44 -0500 Subject: [PATCH 09/15] Fix inclusion of items in doc --- util/update_quick_index.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/util/update_quick_index.py b/util/update_quick_index.py index bcdee6572..07bd2251f 100644 --- a/util/update_quick_index.py +++ b/util/update_quick_index.py @@ -189,7 +189,10 @@ "use_declarations_in": ["arcade.isometric"], }, "anim.rst": { - "title": "Easing", "use_declarations_in": ["arcade.anim.easing"] + "title": "Easing", "use_declarations_in": [ + "arcade.anim", + "arcade.anim.easing" + ] }, "utility.rst": { "title": "Misc Utility Functions", From 07f52aea72e50895be12c8c004edcd2cf73efc59 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 24 Dec 2025 06:02:57 -0500 Subject: [PATCH 10/15] Fix doc build after adding to the quickindex file. * Convert EasingFunction into a typing.Protocol so it gets picked up by doc build + explain why * Correct Sphinx style issues and broken cross-references * Explain how of pyglet.math Matrix types won't work with easing (matmul) * Add an __all__ to arcade.anim.easing --- arcade/anim/easing.py | 150 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 135 insertions(+), 15 deletions(-) diff --git a/arcade/anim/easing.py b/arcade/anim/easing.py index 8c175e2d2..6a6b8731d 100644 --- a/arcade/anim/easing.py +++ b/arcade/anim/easing.py @@ -1,3 +1,4 @@ +"""Core easing annotations and helper functions.""" from collections.abc import Callable from math import cos, pi, sin, sqrt, tau from typing import Protocol, TypeVar @@ -5,7 +6,60 @@ T = TypeVar("T") +# This needs to be a Protocol rather than an annotation +# due to our build configuration being set to pick up +# classes but not type annotations. +class EasingFunction(Protocol): + """Any :py:func:`callable` object which maps linear completion to a curve. + + .. tip:: See :py:class:`Easing` for the most common easings. + + Pass them to :py:func:`.ease` via the ``func`` + keyword argument. + + If the built-in easing curves are not enough, you can define + your own. Functions should match this pattern: + + .. code-block:: python + + def f(t: float) -> t: + ... + + For advanced users, any object with a matching :py:meth:`~object.__call__` + method can be passed as an easing function. + """ + + def __call__(self, __t: float) -> float: + ... + + + class Animatable(Protocol): + """Matches types with support for the following operations: + + .. list-table:: + :header-rows: 1 + + * - Method + - Summary + + * - :py:meth:`~object.__mul__` + - Multiplication by a scalar + + * - :py:meth:`~object.__add__` + - Addition + + * - :py:meth:`~object.__sub__` + - Subtraction + + .. important:: The :py:mod:`pyglet.math` matrix types are currently unsupported. + + Although vector types work, matrix multiplication is + subtly different. It uses a separate :py:meth:`~object.__matmul__` + operator for multiplication. + """ + + def __mul__(self: T, other: T | float, /) -> T: ... def __add__(self: T, other: T | float, /) -> T: ... @@ -30,7 +84,29 @@ def __sub__(self: T, other: T | float, /) -> T: ... class Easing: - """:py:class:`.EasingFunction`s meant for passing into :py:meth:`.ease`.""" + """Built-in easing functions as static methods. + + Each takes the following form: + + .. code-block:: python + + def f(t: float) -> float: + ... + + Pass them into :py:func:`.ease` via the ``func`` keyword + argument: + + .. code-block:: python + + from arcade.anim import ease, Easing + + value = ease( + 1.0, 2.0, + 2.0, 3.0, + 2.4, + func=Easing.SINE_IN) + + """ # This is a bucket of staticmethods because typing. # Enum hates this, and they can't be classmethods. @@ -259,23 +335,33 @@ def _clamp(x: float, low: float, high: float) -> float: def perc(x: float, start: float, end: float) -> float: - """ - Convert a value ``x`` to be a percentage of progression between - ``start`` and ``end``. + """Convert ``x`` to percent-like progress from ``start`` to ``end``. + + Arguments: + x: A value between ``start`` and ``end``. + start: The start of the range. + end: The end of the range. + + Returns: + A normalized percent-like completion as a :py:class:`float`. """ return (x - start) / (end - start) def lerp(x: float, minimum: A, maximum: A) -> A: - """ - Convert a percentage ``x`` to be the value when progressed - that amount between ``minimum`` and ``maximum``. + """Get ``x`` of the way from ``minimum`` to ``maximum``. + + Arguments: + x: A percent-like progress measure from ``0`` to ``1.0``. + minimum: The start value along the path. + maximum: The maximum value along the path. + + Returns: + A value ``x`` of the way from ``minimum`` to ``maximum``. """ return minimum + ((maximum - minimum) * x) -EasingFunction = Callable[[float], float] - def ease( minimum: A, @@ -286,21 +372,55 @@ def ease( func: EasingFunction = Easing.LINEAR, clamped: bool = True, ) -> A: - """Ease a value according to a curve. Useful for animating properties over time. + """Ease a value according to a curve function passed as ``func``. + + Override the default easing curve by passing any :py:class:`.Easing` + or :py:class:`.EasingFunction` of your choice. + + The ``maximum`` and ``minimum`` must be of compatible types. + For example, these can include: + + .. list-table:: + :header-rows: 1 + + * - Type + - Value Example + - Explanation + + * - :py:class:`float` + - ``0.5`` + - Numbers such as volume or brightness. - Args: + * - :py:class:`~pyglet.math.Vec2` + - ``Vec2(500.0, 200.0)`` + - A :py:mod:`pyglet.math` vector representing position. + + Arguments: minimum: any math-like object (a position, scale, value...); the "start position." maximum: any math-like object (a position, scale, value...); the "end position." start: a :py:class:`float` defining where progression begins, the "start time." end: a :py:class:`float` defining where progression ends, the "end time." t: a :py:class:`float` defining the current progression, the "current time." - func: a :py:class:`.EasingFunction` to modify the result with, typically an - attribute of :py:class:`.Easing`. Defaults to :py:attr:`.Easing.LINEAR`. - clamped: a :py:class:`bool`; whether or not to allow the animation to continue past - the ``start`` and ``end`` "times". Defaults to ``True``. + func: Defaults to :py:attr:`Easing.LINEAR`, but you can pass an + :py:class:`Easing` or :py:class:`.EasingFunction` of your choice. + clamped: Whether the value will be clamped to ``minimum`` and ``maximum``. + + Returns: + An eased value for the given time ``t``. + """ p = perc(t, start, end) if clamped: p = _clamp(p, 0.0, 1.0) new_p = func(p) return lerp(new_p, minimum, maximum) + +__all__ = [ + "Animatable", + "Easing", + "EasingFunction", + "ease", + "perc", + "lerp" +] + From aaece2fb7c2aa161cedc76186604ad1f5145763d Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 24 Dec 2025 06:06:39 -0500 Subject: [PATCH 11/15] ./make.py format --- arcade/anim/easing.py | 16 +++------------- util/update_quick_index.py | 7 +------ 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/arcade/anim/easing.py b/arcade/anim/easing.py index 6a6b8731d..ca42ddcd1 100644 --- a/arcade/anim/easing.py +++ b/arcade/anim/easing.py @@ -1,4 +1,5 @@ """Core easing annotations and helper functions.""" + from collections.abc import Callable from math import cos, pi, sin, sqrt, tau from typing import Protocol, TypeVar @@ -29,9 +30,7 @@ def f(t: float) -> t: method can be passed as an easing function. """ - def __call__(self, __t: float) -> float: - ... - + def __call__(self, __t: float) -> float: ... class Animatable(Protocol): @@ -59,7 +58,6 @@ class Animatable(Protocol): operator for multiplication. """ - def __mul__(self: T, other: T | float, /) -> T: ... def __add__(self: T, other: T | float, /) -> T: ... @@ -362,7 +360,6 @@ def lerp(x: float, minimum: A, maximum: A) -> A: return minimum + ((maximum - minimum) * x) - def ease( minimum: A, maximum: A, @@ -415,12 +412,5 @@ def ease( new_p = func(p) return lerp(new_p, minimum, maximum) -__all__ = [ - "Animatable", - "Easing", - "EasingFunction", - "ease", - "perc", - "lerp" -] +__all__ = ["Animatable", "Easing", "EasingFunction", "ease", "perc", "lerp"] diff --git a/util/update_quick_index.py b/util/update_quick_index.py index 07bd2251f..b4fca31cc 100644 --- a/util/update_quick_index.py +++ b/util/update_quick_index.py @@ -188,12 +188,7 @@ "title": "Isometric Map (incomplete)", "use_declarations_in": ["arcade.isometric"], }, - "anim.rst": { - "title": "Easing", "use_declarations_in": [ - "arcade.anim", - "arcade.anim.easing" - ] - }, + "anim.rst": {"title": "Easing", "use_declarations_in": ["arcade.anim", "arcade.anim.easing"]}, "utility.rst": { "title": "Misc Utility Functions", "use_declarations_in": ["arcade", "arcade.__main__", "arcade.utils"], From 886de7cc6e781ba1871bdcd63df7702210de7e28 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 24 Dec 2025 06:11:12 -0500 Subject: [PATCH 12/15] Remove unused typing.Callable import --- arcade/anim/easing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/arcade/anim/easing.py b/arcade/anim/easing.py index ca42ddcd1..27d20844d 100644 --- a/arcade/anim/easing.py +++ b/arcade/anim/easing.py @@ -1,6 +1,4 @@ """Core easing annotations and helper functions.""" - -from collections.abc import Callable from math import cos, pi, sin, sqrt, tau from typing import Protocol, TypeVar From 6d3c8d5c2b216757bc3a7a32edb57543b4b074b9 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 24 Dec 2025 06:13:21 -0500 Subject: [PATCH 13/15] Another ruff formatter run --- arcade/anim/easing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/arcade/anim/easing.py b/arcade/anim/easing.py index 27d20844d..297095726 100644 --- a/arcade/anim/easing.py +++ b/arcade/anim/easing.py @@ -1,4 +1,5 @@ """Core easing annotations and helper functions.""" + from math import cos, pi, sin, sqrt, tau from typing import Protocol, TypeVar From de28a043091a691c5a5a0580354d17684e56ff36 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 24 Dec 2025 06:28:00 -0500 Subject: [PATCH 14/15] Rename perc to norm as discussed --- arcade/anim/__init__.py | 4 ++-- arcade/anim/easing.py | 26 ++++++++++++++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/arcade/anim/__init__.py b/arcade/anim/__init__.py index 4a7bb1e70..e93a038cb 100644 --- a/arcade/anim/__init__.py +++ b/arcade/anim/__init__.py @@ -1,3 +1,3 @@ -from .easing import ease, Easing, lerp, perc +from arcade.anim.easing import ease, Easing, lerp, norm -__all__ = ["ease", "Easing", "lerp", "perc"] +__all__ = ["ease", "Easing", "lerp", "norm"] diff --git a/arcade/anim/easing.py b/arcade/anim/easing.py index 297095726..1b9d34122 100644 --- a/arcade/anim/easing.py +++ b/arcade/anim/easing.py @@ -331,8 +331,13 @@ def _clamp(x: float, low: float, high: float) -> float: return high if x > high else max(x, low) -def perc(x: float, start: float, end: float) -> float: - """Convert ``x`` to percent-like progress from ``start`` to ``end``. +def norm(x: float, start: float, end: float) -> float: + """Convert ``x`` to a progress ratio from ``start`` to ``end``. + + The result will be a value normalized to between ``0.0`` + and ``1.0`` if ``x`` is between ``start`` and ``end`. It + is not clamped, so the result may be less than ``0.0`` or + ``greater than ``1.0``. Arguments: x: A value between ``start`` and ``end``. @@ -340,23 +345,24 @@ def perc(x: float, start: float, end: float) -> float: end: The end of the range. Returns: - A normalized percent-like completion as a :py:class:`float`. + A range completion progress as a :py:class:`float`. """ return (x - start) / (end - start) -def lerp(x: float, minimum: A, maximum: A) -> A: - """Get ``x`` of the way from ``minimum`` to ``maximum``. +def lerp(progress: float, minimum: A, maximum: A) -> A: + """Get ``progress`` of the way from``minimum`` to ``maximum``. Arguments: - x: A percent-like progress measure from ``0`` to ``1.0``. + progress: How far from ``minimum`` to ``maximum`` to go + from ``0.0`` to ``1.0``. minimum: The start value along the path. maximum: The maximum value along the path. Returns: - A value ``x`` of the way from ``minimum`` to ``maximum``. + A value ``progress`` of the way from ``minimum`` to ``maximum``. """ - return minimum + ((maximum - minimum) * x) + return minimum + ((maximum - minimum) * progress) def ease( @@ -405,11 +411,11 @@ def ease( An eased value for the given time ``t``. """ - p = perc(t, start, end) + p = norm(t, start, end) if clamped: p = _clamp(p, 0.0, 1.0) new_p = func(p) return lerp(new_p, minimum, maximum) -__all__ = ["Animatable", "Easing", "EasingFunction", "ease", "perc", "lerp"] +__all__ = ["Animatable", "Easing", "EasingFunction", "ease", "norm", "lerp"] From 8c716d51f34c8e2b7be58bc693a056cf9e73991e Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 24 Dec 2025 13:23:42 -0500 Subject: [PATCH 15/15] Rename `Animatable` to `Interpolatable`, as per @pushfoo --- arcade/anim/easing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arcade/anim/easing.py b/arcade/anim/easing.py index 1b9d34122..a3a12ddce 100644 --- a/arcade/anim/easing.py +++ b/arcade/anim/easing.py @@ -32,7 +32,7 @@ def f(t: float) -> t: def __call__(self, __t: float) -> float: ... -class Animatable(Protocol): +class Interpolatable(Protocol): """Matches types with support for the following operations: .. list-table:: @@ -64,7 +64,7 @@ def __add__(self: T, other: T | float, /) -> T: ... def __sub__(self: T, other: T | float, /) -> T: ... -A = TypeVar("A", bound=Animatable) +A = TypeVar("A", bound=Interpolatable) # === BEGIN EASING FUNCTIONS === @@ -418,4 +418,4 @@ def ease( return lerp(new_p, minimum, maximum) -__all__ = ["Animatable", "Easing", "EasingFunction", "ease", "norm", "lerp"] +__all__ = ["Interpolatable", "Easing", "EasingFunction", "ease", "norm", "lerp"]