1#!/usr/bin/env python3
  2"""
  3Spin Animation - Rotate objects continuously or with variation.
  4
  5Creates spinning, rotating, and wobbling effects.
  6"""
  7
  8import sys
  9from pathlib import Path
 10import math
 11
 12sys.path.append(str(Path(__file__).parent.parent))
 13
 14from PIL import Image
 15from core.gif_builder import GIFBuilder
 16from core.frame_composer import create_blank_frame, draw_emoji_enhanced, draw_circle
 17from core.easing import interpolate
 18
 19
 20def create_spin_animation(
 21    object_type: str = 'emoji',
 22    object_data: dict | None = None,
 23    num_frames: int = 30,
 24    rotation_type: str = 'clockwise',  # 'clockwise', 'counterclockwise', 'wobble', 'pendulum'
 25    full_rotations: float = 1.0,
 26    easing: str = 'linear',
 27    center_pos: tuple[int, int] = (240, 240),
 28    frame_width: int = 480,
 29    frame_height: int = 480,
 30    bg_color: tuple[int, int, int] = (255, 255, 255)
 31) -> list[Image.Image]:
 32    """
 33    Create spinning/rotating animation.
 34
 35    Args:
 36        object_type: 'emoji', 'image', 'text'
 37        object_data: Object configuration
 38        num_frames: Number of frames
 39        rotation_type: Type of rotation
 40        full_rotations: Number of complete 360° rotations
 41        easing: Easing function for rotation speed
 42        center_pos: Center position for rotation
 43        frame_width: Frame width
 44        frame_height: Frame height
 45        bg_color: Background color
 46
 47    Returns:
 48        List of frames
 49    """
 50    frames = []
 51
 52    # Default object data
 53    if object_data is None:
 54        if object_type == 'emoji':
 55            object_data = {'emoji': '🔄', 'size': 100}
 56
 57    for i in range(num_frames):
 58        frame = create_blank_frame(frame_width, frame_height, bg_color)
 59        t = i / (num_frames - 1) if num_frames > 1 else 0
 60
 61        # Calculate rotation angle
 62        if rotation_type == 'clockwise':
 63            angle = interpolate(0, 360 * full_rotations, t, easing)
 64        elif rotation_type == 'counterclockwise':
 65            angle = interpolate(0, -360 * full_rotations, t, easing)
 66        elif rotation_type == 'wobble':
 67            # Back and forth rotation
 68            angle = math.sin(t * full_rotations * 2 * math.pi) * 45
 69        elif rotation_type == 'pendulum':
 70            # Smooth pendulum swing
 71            angle = math.sin(t * full_rotations * 2 * math.pi) * 90
 72        else:
 73            angle = interpolate(0, 360 * full_rotations, t, easing)
 74
 75        # Create object on transparent background to rotate
 76        if object_type == 'emoji':
 77            # For emoji, we need to create a larger canvas to avoid clipping during rotation
 78            emoji_size = object_data['size']
 79            canvas_size = int(emoji_size * 1.5)
 80            emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
 81
 82            # Draw emoji in center of canvas
 83            from core.frame_composer import draw_emoji_enhanced
 84            draw_emoji_enhanced(
 85                emoji_canvas,
 86                emoji=object_data['emoji'],
 87                position=(canvas_size // 2 - emoji_size // 2, canvas_size // 2 - emoji_size // 2),
 88                size=emoji_size,
 89                shadow=False
 90            )
 91
 92            # Rotate the canvas
 93            rotated = emoji_canvas.rotate(angle, resample=Image.BICUBIC, expand=False)
 94
 95            # Paste onto frame
 96            paste_x = center_pos[0] - canvas_size // 2
 97            paste_y = center_pos[1] - canvas_size // 2
 98            frame.paste(rotated, (paste_x, paste_y), rotated)
 99
100        elif object_type == 'text':
101            from core.typography import draw_text_with_outline
102            # Similar approach - create canvas, draw text, rotate
103            text = object_data.get('text', 'SPIN!')
104            font_size = object_data.get('font_size', 50)
105
106            canvas_size = max(frame_width, frame_height)
107            text_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
108
109            # Draw text
110            text_canvas_rgb = text_canvas.convert('RGB')
111            text_canvas_rgb.paste(bg_color, (0, 0, canvas_size, canvas_size))
112            draw_text_with_outline(
113                text_canvas_rgb,
114                text,
115                position=(canvas_size // 2, canvas_size // 2),
116                font_size=font_size,
117                text_color=object_data.get('text_color', (0, 0, 0)),
118                outline_color=object_data.get('outline_color', (255, 255, 255)),
119                outline_width=3,
120                centered=True
121            )
122
123            # Convert back to RGBA for rotation
124            text_canvas = text_canvas_rgb.convert('RGBA')
125
126            # Make background transparent
127            data = text_canvas.getdata()
128            new_data = []
129            for item in data:
130                if item[:3] == bg_color:
131                    new_data.append((255, 255, 255, 0))
132                else:
133                    new_data.append(item)
134            text_canvas.putdata(new_data)
135
136            # Rotate
137            rotated = text_canvas.rotate(angle, resample=Image.BICUBIC, expand=False)
138
139            # Composite onto frame
140            frame_rgba = frame.convert('RGBA')
141            frame_rgba = Image.alpha_composite(frame_rgba, rotated)
142            frame = frame_rgba.convert('RGB')
143
144        frames.append(frame)
145
146    return frames
147
148
149def create_loading_spinner(
150    num_frames: int = 20,
151    spinner_type: str = 'dots',  # 'dots', 'arc', 'emoji'
152    size: int = 100,
153    color: tuple[int, int, int] = (100, 150, 255),
154    frame_width: int = 128,
155    frame_height: int = 128,
156    bg_color: tuple[int, int, int] = (255, 255, 255)
157) -> list[Image.Image]:
158    """
159    Create a loading spinner animation.
160
161    Args:
162        num_frames: Number of frames
163        spinner_type: Type of spinner
164        size: Spinner size
165        color: Spinner color
166        frame_width: Frame width
167        frame_height: Frame height
168        bg_color: Background color
169
170    Returns:
171        List of frames
172    """
173    from PIL import ImageDraw
174    frames = []
175    center = (frame_width // 2, frame_height // 2)
176
177    for i in range(num_frames):
178        frame = create_blank_frame(frame_width, frame_height, bg_color)
179        draw = ImageDraw.Draw(frame)
180
181        angle_offset = (i / num_frames) * 360
182
183        if spinner_type == 'dots':
184            # Circular dots
185            num_dots = 8
186            for j in range(num_dots):
187                angle = (j / num_dots * 360 + angle_offset) * math.pi / 180
188                x = center[0] + size * 0.4 * math.cos(angle)
189                y = center[1] + size * 0.4 * math.sin(angle)
190
191                # Fade based on position
192                alpha = 1.0 - (j / num_dots)
193                dot_color = tuple(int(c * alpha) for c in color)
194                dot_radius = int(size * 0.1)
195
196                draw.ellipse(
197                    [x - dot_radius, y - dot_radius, x + dot_radius, y + dot_radius],
198                    fill=dot_color
199                )
200
201        elif spinner_type == 'arc':
202            # Rotating arc
203            start_angle = angle_offset
204            end_angle = angle_offset + 270
205            arc_width = int(size * 0.15)
206
207            bbox = [
208                center[0] - size // 2,
209                center[1] - size // 2,
210                center[0] + size // 2,
211                center[1] + size // 2
212            ]
213            draw.arc(bbox, start_angle, end_angle, fill=color, width=arc_width)
214
215        elif spinner_type == 'emoji':
216            # Rotating emoji spinner
217            angle = angle_offset
218            emoji_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
219            draw_emoji_enhanced(
220                emoji_canvas,
221                emoji='',
222                position=(center[0] - size // 2, center[1] - size // 2),
223                size=size,
224                shadow=False
225            )
226            rotated = emoji_canvas.rotate(angle, center=center, resample=Image.BICUBIC)
227            frame.paste(rotated, (0, 0), rotated)
228
229        frames.append(frame)
230
231    return frames
232
233
234# Example usage
235if __name__ == '__main__':
236    print("Creating spin animations...")
237
238    builder = GIFBuilder(width=480, height=480, fps=20)
239
240    # Example 1: Clockwise spin
241    frames = create_spin_animation(
242        object_type='emoji',
243        object_data={'emoji': '🔄', 'size': 100},
244        num_frames=30,
245        rotation_type='clockwise',
246        full_rotations=2
247    )
248    builder.add_frames(frames)
249    builder.save('spin_clockwise.gif', num_colors=128)
250
251    # Example 2: Wobble
252    builder.clear()
253    frames = create_spin_animation(
254        object_type='emoji',
255        object_data={'emoji': '🎯', 'size': 100},
256        num_frames=30,
257        rotation_type='wobble',
258        full_rotations=3
259    )
260    builder.add_frames(frames)
261    builder.save('spin_wobble.gif', num_colors=128)
262
263    # Example 3: Loading spinner
264    builder = GIFBuilder(width=128, height=128, fps=15)
265    frames = create_loading_spinner(num_frames=20, spinner_type='dots')
266    builder.add_frames(frames)
267    builder.save('loading_spinner.gif', num_colors=64, optimize_for_emoji=True)
268
269    print("Created spin animations!")