main
1#!/usr/bin/env python3
2"""
3Move Animation - Move objects along paths with various motion types.
4
5Provides flexible movement primitives for objects along linear, arc, or custom paths.
6"""
7
8import sys
9from pathlib import Path
10import math
11
12sys.path.append(str(Path(__file__).parent.parent))
13
14from core.gif_builder import GIFBuilder
15from core.frame_composer import create_blank_frame, draw_circle, draw_emoji_enhanced
16from core.easing import interpolate, calculate_arc_motion
17
18
19def create_move_animation(
20 object_type: str = 'emoji',
21 object_data: dict | None = None,
22 start_pos: tuple[int, int] = (50, 240),
23 end_pos: tuple[int, int] = (430, 240),
24 num_frames: int = 30,
25 motion_type: str = 'linear', # 'linear', 'arc', 'bezier', 'circle', 'wave'
26 easing: str = 'ease_out',
27 motion_params: dict | None = None,
28 frame_width: int = 480,
29 frame_height: int = 480,
30 bg_color: tuple[int, int, int] = (255, 255, 255)
31) -> list:
32 """
33 Create frames showing object moving along a path.
34
35 Args:
36 object_type: 'circle', 'emoji', or 'custom'
37 object_data: Data for the object
38 start_pos: Starting (x, y) position
39 end_pos: Ending (x, y) position
40 num_frames: Number of frames
41 motion_type: Type of motion path
42 easing: Easing function name
43 motion_params: Additional parameters for motion (e.g., {'arc_height': 100})
44 frame_width: Frame width
45 frame_height: Frame height
46 bg_color: Background color
47
48 Returns:
49 List of frames
50 """
51 frames = []
52
53 # Default object data
54 if object_data is None:
55 if object_type == 'circle':
56 object_data = {'radius': 30, 'color': (100, 150, 255)}
57 elif object_type == 'emoji':
58 object_data = {'emoji': '🚀', 'size': 60}
59
60 # Default motion params
61 if motion_params is None:
62 motion_params = {}
63
64 for i in range(num_frames):
65 frame = create_blank_frame(frame_width, frame_height, bg_color)
66
67 t = i / (num_frames - 1) if num_frames > 1 else 0
68
69 # Calculate position based on motion type
70 if motion_type == 'linear':
71 # Straight line with easing
72 x = interpolate(start_pos[0], end_pos[0], t, easing)
73 y = interpolate(start_pos[1], end_pos[1], t, easing)
74
75 elif motion_type == 'arc':
76 # Parabolic arc
77 arc_height = motion_params.get('arc_height', 100)
78 x, y = calculate_arc_motion(start_pos, end_pos, arc_height, t)
79
80 elif motion_type == 'circle':
81 # Circular motion around a center
82 center = motion_params.get('center', (frame_width // 2, frame_height // 2))
83 radius = motion_params.get('radius', 150)
84 start_angle = motion_params.get('start_angle', 0)
85 angle_range = motion_params.get('angle_range', 360) # Full circle
86
87 angle = start_angle + (angle_range * t)
88 angle_rad = math.radians(angle)
89
90 x = center[0] + radius * math.cos(angle_rad)
91 y = center[1] + radius * math.sin(angle_rad)
92
93 elif motion_type == 'wave':
94 # Move in straight line but add wave motion
95 wave_amplitude = motion_params.get('wave_amplitude', 50)
96 wave_frequency = motion_params.get('wave_frequency', 2)
97
98 # Base linear motion
99 base_x = interpolate(start_pos[0], end_pos[0], t, easing)
100 base_y = interpolate(start_pos[1], end_pos[1], t, easing)
101
102 # Add wave offset perpendicular to motion direction
103 dx = end_pos[0] - start_pos[0]
104 dy = end_pos[1] - start_pos[1]
105 length = math.sqrt(dx * dx + dy * dy)
106
107 if length > 0:
108 # Perpendicular direction
109 perp_x = -dy / length
110 perp_y = dx / length
111
112 # Wave offset
113 wave_offset = math.sin(t * wave_frequency * 2 * math.pi) * wave_amplitude
114
115 x = base_x + perp_x * wave_offset
116 y = base_y + perp_y * wave_offset
117 else:
118 x, y = base_x, base_y
119
120 elif motion_type == 'bezier':
121 # Quadratic bezier curve
122 control_point = motion_params.get('control_point', (
123 (start_pos[0] + end_pos[0]) // 2,
124 (start_pos[1] + end_pos[1]) // 2 - 100
125 ))
126
127 # Quadratic Bezier formula: B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2
128 x = (1 - t) ** 2 * start_pos[0] + 2 * (1 - t) * t * control_point[0] + t ** 2 * end_pos[0]
129 y = (1 - t) ** 2 * start_pos[1] + 2 * (1 - t) * t * control_point[1] + t ** 2 * end_pos[1]
130
131 else:
132 # Default to linear
133 x = interpolate(start_pos[0], end_pos[0], t, easing)
134 y = interpolate(start_pos[1], end_pos[1], t, easing)
135
136 # Draw object at calculated position
137 x, y = int(x), int(y)
138
139 if object_type == 'circle':
140 draw_circle(
141 frame,
142 center=(x, y),
143 radius=object_data['radius'],
144 fill_color=object_data['color']
145 )
146 elif object_type == 'emoji':
147 draw_emoji_enhanced(
148 frame,
149 emoji=object_data['emoji'],
150 position=(x - object_data['size'] // 2, y - object_data['size'] // 2),
151 size=object_data['size'],
152 shadow=object_data.get('shadow', True)
153 )
154
155 frames.append(frame)
156
157 return frames
158
159
160def create_path_from_points(points: list[tuple[int, int]],
161 num_frames: int = 60,
162 easing: str = 'ease_in_out') -> list[tuple[int, int]]:
163 """
164 Create a smooth path through multiple points.
165
166 Args:
167 points: List of (x, y) waypoints
168 num_frames: Total number of frames
169 easing: Easing between points
170
171 Returns:
172 List of (x, y) positions for each frame
173 """
174 if len(points) < 2:
175 return points * num_frames
176
177 path = []
178 frames_per_segment = num_frames // (len(points) - 1)
179
180 for i in range(len(points) - 1):
181 start = points[i]
182 end = points[i + 1]
183
184 # Last segment gets remaining frames
185 if i == len(points) - 2:
186 segment_frames = num_frames - len(path)
187 else:
188 segment_frames = frames_per_segment
189
190 for j in range(segment_frames):
191 t = j / segment_frames if segment_frames > 0 else 0
192 x = interpolate(start[0], end[0], t, easing)
193 y = interpolate(start[1], end[1], t, easing)
194 path.append((int(x), int(y)))
195
196 return path
197
198
199def apply_trail_effect(frames: list, trail_length: int = 5,
200 fade_alpha: float = 0.3) -> list:
201 """
202 Add motion trail effect to moving object.
203
204 Args:
205 frames: List of frames with moving object
206 trail_length: Number of previous frames to blend
207 fade_alpha: Opacity of trail frames
208
209 Returns:
210 List of frames with trail effect
211 """
212 from PIL import Image, ImageChops
213 import numpy as np
214
215 trailed_frames = []
216
217 for i, frame in enumerate(frames):
218 # Start with current frame
219 result = frame.copy()
220
221 # Blend previous frames
222 for j in range(1, min(trail_length + 1, i + 1)):
223 prev_frame = frames[i - j]
224
225 # Calculate fade
226 alpha = fade_alpha ** j
227
228 # Blend
229 result_array = np.array(result, dtype=np.float32)
230 prev_array = np.array(prev_frame, dtype=np.float32)
231
232 blended = result_array * (1 - alpha) + prev_array * alpha
233 result = Image.fromarray(blended.astype(np.uint8))
234
235 trailed_frames.append(result)
236
237 return trailed_frames
238
239
240# Example usage
241if __name__ == '__main__':
242 print("Creating movement examples...")
243
244 # Example 1: Linear movement
245 builder = GIFBuilder(width=480, height=480, fps=20)
246 frames = create_move_animation(
247 object_type='emoji',
248 object_data={'emoji': '🚀', 'size': 60},
249 start_pos=(50, 240),
250 end_pos=(430, 240),
251 num_frames=30,
252 motion_type='linear',
253 easing='ease_out'
254 )
255 builder.add_frames(frames)
256 builder.save('move_linear.gif', num_colors=128)
257
258 # Example 2: Arc movement
259 builder.clear()
260 frames = create_move_animation(
261 object_type='emoji',
262 object_data={'emoji': '⚽', 'size': 60},
263 start_pos=(50, 350),
264 end_pos=(430, 350),
265 num_frames=30,
266 motion_type='arc',
267 motion_params={'arc_height': 150},
268 easing='linear'
269 )
270 builder.add_frames(frames)
271 builder.save('move_arc.gif', num_colors=128)
272
273 # Example 3: Circular movement
274 builder.clear()
275 frames = create_move_animation(
276 object_type='emoji',
277 object_data={'emoji': '🌍', 'size': 50},
278 start_pos=(0, 0), # Ignored for circle
279 end_pos=(0, 0), # Ignored for circle
280 num_frames=40,
281 motion_type='circle',
282 motion_params={
283 'center': (240, 240),
284 'radius': 120,
285 'start_angle': 0,
286 'angle_range': 360
287 },
288 easing='linear'
289 )
290 builder.add_frames(frames)
291 builder.save('move_circle.gif', num_colors=128)
292
293 print("Created movement examples!")