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!")