diff --git a/arcade/anim/__init__.py b/arcade/anim/__init__.py new file mode 100644 index 000000000..e93a038cb --- /dev/null +++ b/arcade/anim/__init__.py @@ -0,0 +1,3 @@ +from arcade.anim.easing import ease, Easing, lerp, norm + +__all__ = ["ease", "Easing", "lerp", "norm"] diff --git a/arcade/anim/easing.py b/arcade/anim/easing.py new file mode 100644 index 000000000..a3a12ddce --- /dev/null +++ b/arcade/anim/easing.py @@ -0,0 +1,421 @@ +"""Core easing annotations and helper functions.""" + +from math import cos, pi, sin, sqrt, tau +from typing import Protocol, TypeVar + +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 Interpolatable(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: ... + + def __sub__(self: T, other: T | float, /) -> T: ... + + +A = TypeVar("A", bound=Interpolatable) + +# === BEGIN EASING FUNCTIONS === + +# 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 +D1 = 2.75 + + +class Easing: + """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. + # That's why their capitalized, it's meant to be an Enum-like + # Sorry that this looks strange! -- DigiDuncan + + @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 + 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 === + + +def _clamp(x: float, low: float, high: float) -> float: + return high if x > high else max(x, low) + + +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``. + start: The start of the range. + end: The end of the range. + + Returns: + A range completion progress as a :py:class:`float`. + """ + return (x - start) / (end - start) + + +def lerp(progress: float, minimum: A, maximum: A) -> A: + """Get ``progress`` of the way from``minimum`` to ``maximum``. + + Arguments: + 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 ``progress`` of the way from ``minimum`` to ``maximum``. + """ + return minimum + ((maximum - minimum) * progress) + + +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 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. + + * - :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: 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 = norm(t, start, end) + if clamped: + p = _clamp(p, 0.0, 1.0) + new_p = func(p) + return lerp(new_p, minimum, maximum) + + +__all__ = ["Interpolatable", "Easing", "EasingFunction", "ease", "norm", "lerp"] 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 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() 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..b4fca31cc 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", "arcade.anim.easing"]}, "utility.rst": { "title": "Misc Utility Functions", "use_declarations_in": ["arcade", "arcade.__main__", "arcade.utils"],