Easing Example 2

Screen shot of Easing Example 2
easing_example_2.py
  1"""
  2Easing Example 2
  3
  4Demonstrate angle and position easing on a ship sprite that turns and moves
  5toward the mouse cursor.  Press number keys to switch between modes:
  6
  7- **1**: Instant angle (no easing)
  8- **2-5**: Angle easing (LINEAR, QUAD_IN, QUAD_OUT, SINE)
  9- **6-9**: Position easing (LINEAR, QUAD_IN, QUAD_OUT, SINE)
 10
 11If Python and Arcade are installed, this example can be run from the command line with:
 12python -m arcade.examples.easing_example_2
 13"""
 14
 15import math
 16import arcade
 17from arcade.anim import ease, Easing
 18
 19# --- Constants ---
 20WINDOW_WIDTH = 1280
 21WINDOW_HEIGHT = 720
 22WINDOW_TITLE = "Easing Example 2"
 23
 24SHIP_SPEED = 5.0
 25EASE_DURATION = 1.0
 26
 27# Mode descriptions shown in the HUD.
 28MODE_DESCRIPTIONS = {
 29    1: "Instant (teleport + face mouse)",
 30    2: "Angle ease: LINEAR",
 31    3: "Angle ease: QUAD_IN",
 32    4: "Angle ease: QUAD_OUT",
 33    5: "Angle ease: SINE",
 34    6: "Position ease: LINEAR",
 35    7: "Position ease: QUAD_IN",
 36    8: "Position ease: QUAD_OUT",
 37    9: "Position ease: SINE",
 38}
 39
 40# Mapping from mode number to easing function (modes 2-9).
 41MODE_EASING = {
 42    2: Easing.LINEAR,
 43    3: Easing.QUAD_IN,
 44    4: Easing.QUAD_OUT,
 45    5: Easing.SINE,
 46    6: Easing.LINEAR,
 47    7: Easing.QUAD_IN,
 48    8: Easing.QUAD_OUT,
 49    9: Easing.SINE,
 50}
 51
 52
 53def shortest_angle_delta(from_angle: float, to_angle: float) -> float:
 54    """Return the shortest signed rotation from *from_angle* to *to_angle*.
 55
 56    Both angles are in degrees.  The result is in the range (-180, 180].
 57    """
 58    delta = (to_angle - from_angle) % 360
 59    if delta > 180:
 60        delta -= 360
 61    return delta
 62
 63
 64class GameView(arcade.View):
 65    """Main view with a ship that eases toward the mouse."""
 66
 67    def __init__(self):
 68        super().__init__()
 69        self.background_color = arcade.color.BLACK
 70
 71        self.ship_sprite: arcade.Sprite | None = None
 72        self.mode = 1
 73        self.time_elapsed = 0.0
 74
 75        # Mouse target
 76        self.target_x = WINDOW_WIDTH / 2
 77        self.target_y = WINDOW_HEIGHT / 2
 78
 79        # Angle easing state
 80        self.angle_start = 0.0
 81        self.angle_end = 0.0
 82        self.angle_ease_start_time = 0.0
 83
 84        # Position easing state
 85        self.pos_start_x = WINDOW_WIDTH / 2
 86        self.pos_start_y = WINDOW_HEIGHT / 2
 87        self.pos_end_x = WINDOW_WIDTH / 2
 88        self.pos_end_y = WINDOW_HEIGHT / 2
 89        self.pos_ease_start_time = 0.0
 90
 91    def setup(self):
 92        """Set up the game."""
 93        self.ship_sprite = arcade.Sprite(
 94            ":resources:images/space_shooter/playerShip1_orange.png",
 95            scale=0.5,
 96        )
 97        self.ship_sprite.center_x = WINDOW_WIDTH / 2
 98        self.ship_sprite.center_y = WINDOW_HEIGHT / 2
 99        self.time_elapsed = 0.0
100
101    def on_draw(self):
102        """Render the scene."""
103        self.clear()
104
105        # Draw the ship
106        arcade.draw_sprite(self.ship_sprite)
107
108        # Draw a crosshair at the target
109        arcade.draw_circle_outline(
110            self.target_x, self.target_y, 10, arcade.color.RED, 2,
111        )
112
113        # HUD
114        description = MODE_DESCRIPTIONS.get(self.mode, "")
115        arcade.draw_text(
116            f"Mode {self.mode}: {description}",
117            10, WINDOW_HEIGHT - 30,
118            color=arcade.color.WHITE,
119            font_size=16,
120        )
121        arcade.draw_text(
122            "Press 1-9 to change mode.  Click to set target.",
123            10, WINDOW_HEIGHT - 55,
124            color=arcade.color.GRAY,
125            font_size=12,
126        )
127
128    def _target_angle(self) -> float:
129        """Compute the angle from the ship to the target in degrees.
130
131        Arcade uses clockwise-positive angles and the ship sprite
132        points up at angle 0, so we negate atan2 and add 90.
133        """
134        diff_x = self.target_x - self.ship_sprite.center_x
135        diff_y = self.target_y - self.ship_sprite.center_y
136        return -math.degrees(math.atan2(diff_y, diff_x)) + 90
137
138    def _start_angle_ease(self):
139        """Record the current angle as the start and set up the ease."""
140        self.angle_start = self.ship_sprite.angle
141        target = self._target_angle()
142        delta = shortest_angle_delta(self.angle_start, target)
143        self.angle_end = self.angle_start + delta
144        self.angle_ease_start_time = self.time_elapsed
145
146    def _start_position_ease(self):
147        """Record the current position as the start and set up the ease."""
148        self.pos_start_x = self.ship_sprite.center_x
149        self.pos_start_y = self.ship_sprite.center_y
150        self.pos_end_x = self.target_x
151        self.pos_end_y = self.target_y
152        self.pos_ease_start_time = self.time_elapsed
153
154    def on_update(self, delta_time: float):
155        """Update ship angle and/or position based on current mode."""
156        self.time_elapsed += delta_time
157        ease_func = MODE_EASING.get(self.mode)
158
159        if self.mode == 1:
160            # Instant angle — always face the target directly
161            self.ship_sprite.angle = self._target_angle()
162
163        elif 2 <= self.mode <= 5:
164            # Angle easing
165            eased_angle = ease(
166                self.angle_start, self.angle_end,
167                self.angle_ease_start_time,
168                self.angle_ease_start_time + EASE_DURATION,
169                self.time_elapsed,
170                func=ease_func,
171            )
172            self.ship_sprite.angle = eased_angle
173
174        elif 6 <= self.mode <= 9:
175            # Position easing — also face the target instantly
176            self.ship_sprite.angle = self._target_angle()
177
178            eased_x = ease(
179                self.pos_start_x, self.pos_end_x,
180                self.pos_ease_start_time,
181                self.pos_ease_start_time + EASE_DURATION,
182                self.time_elapsed,
183                func=ease_func,
184            )
185            eased_y = ease(
186                self.pos_start_y, self.pos_end_y,
187                self.pos_ease_start_time,
188                self.pos_ease_start_time + EASE_DURATION,
189                self.time_elapsed,
190                func=ease_func,
191            )
192            self.ship_sprite.center_x = eased_x
193            self.ship_sprite.center_y = eased_y
194
195    def on_mouse_press(self, x: int, y: int, button: int, modifiers: int):
196        """Set a new target and begin an easing animation."""
197        self.target_x = x
198        self.target_y = y
199
200        if self.mode == 1:
201            self.ship_sprite.center_x = x
202            self.ship_sprite.center_y = y
203        elif 2 <= self.mode <= 5:
204            self._start_angle_ease()
205        elif 6 <= self.mode <= 9:
206            self._start_position_ease()
207
208    def on_mouse_motion(self, x: int, y: int, dx: int, dy: int):
209        """Update target for instant-angle mode."""
210        if self.mode == 1:
211            self.target_x = x
212            self.target_y = y
213
214    def on_key_press(self, key: int, modifiers: int):
215        """Switch modes with number keys 1-9."""
216        key_map = {
217            arcade.key.KEY_1: 1, arcade.key.KEY_2: 2, arcade.key.KEY_3: 3,
218            arcade.key.KEY_4: 4, arcade.key.KEY_5: 5, arcade.key.KEY_6: 6,
219            arcade.key.KEY_7: 7, arcade.key.KEY_8: 8, arcade.key.KEY_9: 9,
220        }
221        new_mode = key_map.get(key)
222        if new_mode is not None:
223            self.mode = new_mode
224            if 2 <= new_mode <= 5:
225                self._start_angle_ease()
226            elif 6 <= new_mode <= 9:
227                self._start_position_ease()
228
229
230def main():
231    """Main function."""
232    window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
233    game = GameView()
234    game.setup()
235    window.show_view(game)
236    arcade.run()
237
238
239if __name__ == "__main__":
240    main()