Skip to content

Commit 2f5363f

Browse files
pvcravenPaul V Cravenclaude
authored
Restore easing examples updated for arcade.anim API (#2852)
* Restore easing examples updated for arcade.anim API Rewrites the old easing_example_1 and easing_example_2 programs to use the new stateless arcade.anim.ease() function and Easing class instead of the removed arcade.easing module. Adds RST doc pages and updates the example index. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix example 2 --------- Co-authored-by: Paul V Craven <paul.craven@optimizley.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c278d2a commit 2f5363f

5 files changed

Lines changed: 423 additions & 2 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""
2+
Easing Example 1
3+
4+
Demonstrate the different easing functions available in :py:mod:`arcade.anim`.
5+
Each ball uses a different easing curve to travel from left to right, making it
6+
easy to compare the visual character of each curve.
7+
8+
If Python and Arcade are installed, this example can be run from the command line with:
9+
python -m arcade.examples.easing_example_1
10+
"""
11+
12+
import arcade
13+
from arcade.anim import ease, Easing
14+
15+
# --- Constants ---
16+
WINDOW_WIDTH = 1280
17+
WINDOW_HEIGHT = 720
18+
WINDOW_TITLE = "Easing Example 1"
19+
20+
X_START = 40
21+
X_END = 1200
22+
Y_INTERVAL = 60
23+
BALL_RADIUS = 13
24+
LINE_WIDTH = 1.0
25+
TRAVEL_TIME = 3.0
26+
27+
BACKGROUND_COLOR = arcade.types.Color.from_hex_string("#F5D167")
28+
TEXT_COLOR = arcade.types.Color.from_hex_string("#4B1DF2")
29+
BALL_COLOR = arcade.types.Color.from_hex_string("#42B5EB")
30+
LINE_COLOR = arcade.types.Color.from_hex_string("#45E6D0")
31+
32+
# Each entry is (label, easing function).
33+
EASING_LIST = [
34+
("LINEAR", Easing.LINEAR),
35+
("QUAD_OUT", Easing.QUAD_OUT),
36+
("QUAD_IN", Easing.QUAD_IN),
37+
("SINE", Easing.SINE),
38+
("QUAD", Easing.QUAD),
39+
("ELASTIC_OUT", Easing.ELASTIC_OUT),
40+
("BACK_IN", Easing.BACK_IN),
41+
("BACK_OUT", Easing.BACK_OUT),
42+
("SINE_IN", Easing.SINE_IN),
43+
("SINE_OUT", Easing.SINE_OUT),
44+
("BOUNCE_OUT", Easing.BOUNCE_OUT),
45+
]
46+
47+
48+
class EasingCircle(arcade.SpriteCircle):
49+
"""A ball that eases along the x-axis using a specific curve."""
50+
51+
def __init__(self, radius: int, color: arcade.types.RGBOrA255,
52+
ease_function: Easing):
53+
super().__init__(radius, color)
54+
self.ease_function = ease_function
55+
self.start_time = 0.0
56+
57+
58+
class GameView(arcade.View):
59+
"""Main view showing all easing balls."""
60+
61+
def __init__(self):
62+
super().__init__()
63+
self.background_color = BACKGROUND_COLOR
64+
self.ball_list: arcade.SpriteList[EasingCircle] | None = None
65+
self.time_elapsed = 0.0
66+
67+
def setup(self):
68+
"""Create one ball per easing function."""
69+
self.ball_list = arcade.SpriteList()
70+
self.time_elapsed = 0.0
71+
72+
for index, (label, ease_func) in enumerate(EASING_LIST):
73+
ball = EasingCircle(BALL_RADIUS, BALL_COLOR, ease_func)
74+
ball_y = WINDOW_HEIGHT - (index + 1) * Y_INTERVAL
75+
ball.center_x = X_START
76+
ball.center_y = ball_y
77+
ball.start_time = 0.0
78+
self.ball_list.append(ball)
79+
80+
def on_draw(self):
81+
"""Render the scene."""
82+
self.clear()
83+
84+
for index, (label, _ease_func) in enumerate(EASING_LIST):
85+
ball_y = WINDOW_HEIGHT - (index + 1) * Y_INTERVAL
86+
87+
# Horizontal guide line
88+
arcade.draw_line(X_START, ball_y, X_END, ball_y, LINE_COLOR, LINE_WIDTH)
89+
90+
# Label for this easing function
91+
arcade.draw_text(
92+
label,
93+
X_END + 10,
94+
ball_y,
95+
color=TEXT_COLOR,
96+
font_size=10,
97+
anchor_y="center",
98+
)
99+
100+
# Draw all balls
101+
self.ball_list.draw()
102+
103+
# Instructions
104+
arcade.draw_text(
105+
"Click to restart",
106+
WINDOW_WIDTH // 2,
107+
20,
108+
color=TEXT_COLOR,
109+
font_size=14,
110+
anchor_x="center",
111+
anchor_y="center",
112+
)
113+
114+
def on_update(self, delta_time: float):
115+
"""Update ball positions using the easing functions."""
116+
self.time_elapsed += delta_time
117+
118+
for ball in self.ball_list:
119+
eased_x = ease(
120+
X_START, X_END,
121+
ball.start_time, ball.start_time + TRAVEL_TIME,
122+
self.time_elapsed,
123+
func=ball.ease_function,
124+
)
125+
ball.center_x = eased_x
126+
127+
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int):
128+
"""Restart the animation on click."""
129+
for ball in self.ball_list:
130+
ball.start_time = self.time_elapsed
131+
132+
133+
def main():
134+
"""Main function."""
135+
window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
136+
game = GameView()
137+
game.setup()
138+
window.show_view(game)
139+
arcade.run()
140+
141+
142+
if __name__ == "__main__":
143+
main()
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
"""
2+
Easing Example 2
3+
4+
Demonstrate angle and position easing on a ship sprite that turns and moves
5+
toward 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+
11+
If Python and Arcade are installed, this example can be run from the command line with:
12+
python -m arcade.examples.easing_example_2
13+
"""
14+
15+
import math
16+
import arcade
17+
from arcade.anim import ease, Easing
18+
19+
# --- Constants ---
20+
WINDOW_WIDTH = 1280
21+
WINDOW_HEIGHT = 720
22+
WINDOW_TITLE = "Easing Example 2"
23+
24+
SHIP_SPEED = 5.0
25+
EASE_DURATION = 1.0
26+
27+
# Mode descriptions shown in the HUD.
28+
MODE_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).
41+
MODE_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+
53+
def 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+
64+
class 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+
230+
def 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+
239+
if __name__ == "__main__":
240+
main()

0 commit comments

Comments
 (0)