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()