1#!/usr/bin/env python3
  2"""
  3Wiggle Animation - Smooth, organic wobbling and jiggling motions.
  4
  5Creates playful, elastic movements that are smoother than shake.
  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
 17from core.easing import interpolate
 18
 19
 20def create_wiggle_animation(
 21    object_type: str = 'emoji',
 22    object_data: dict | None = None,
 23    num_frames: int = 30,
 24    wiggle_type: str = 'jello',  # 'jello', 'wave', 'bounce', 'sway'
 25    intensity: float = 1.0,
 26    cycles: float = 2.0,
 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 wiggle/wobble animation.
 34
 35    Args:
 36        object_type: 'emoji', 'text'
 37        object_data: Object configuration
 38        num_frames: Number of frames
 39        wiggle_type: Type of wiggle motion
 40        intensity: Wiggle intensity multiplier
 41        cycles: Number of wiggle cycles
 42        center_pos: Center position
 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        t = i / (num_frames - 1) if num_frames > 1 else 0
 59        frame = create_blank_frame(frame_width, frame_height, bg_color)
 60
 61        # Calculate wiggle transformations
 62        offset_x = 0
 63        offset_y = 0
 64        rotation = 0
 65        scale_x = 1.0
 66        scale_y = 1.0
 67
 68        if wiggle_type == 'jello':
 69            # Jello wobble - multiple frequencies
 70            freq1 = cycles * 2 * math.pi
 71            freq2 = cycles * 3 * math.pi
 72            freq3 = cycles * 5 * math.pi
 73
 74            decay = 1.0 - t if cycles < 1.5 else 1.0  # Decay for single wiggles
 75
 76            offset_x = (
 77                math.sin(freq1 * t) * 15 +
 78                math.sin(freq2 * t) * 8 +
 79                math.sin(freq3 * t) * 3
 80            ) * intensity * decay
 81
 82            rotation = (
 83                math.sin(freq1 * t) * 10 +
 84                math.cos(freq2 * t) * 5
 85            ) * intensity * decay
 86
 87            # Squash and stretch
 88            scale_y = 1.0 + math.sin(freq1 * t) * 0.1 * intensity * decay
 89            scale_x = 1.0 / scale_y  # Preserve volume
 90
 91        elif wiggle_type == 'wave':
 92            # Wave motion
 93            freq = cycles * 2 * math.pi
 94            offset_y = math.sin(freq * t) * 20 * intensity
 95            rotation = math.sin(freq * t + math.pi / 4) * 8 * intensity
 96
 97        elif wiggle_type == 'bounce':
 98            # Bouncy wiggle
 99            freq = cycles * 2 * math.pi
100            bounce = abs(math.sin(freq * t))
101
102            scale_y = 1.0 + bounce * 0.2 * intensity
103            scale_x = 1.0 - bounce * 0.1 * intensity
104            offset_y = -bounce * 10 * intensity
105
106        elif wiggle_type == 'sway':
107            # Gentle sway back and forth
108            freq = cycles * 2 * math.pi
109            offset_x = math.sin(freq * t) * 25 * intensity
110            rotation = math.sin(freq * t) * 12 * intensity
111
112            # Subtle scale change
113            scale = 1.0 + math.sin(freq * t) * 0.05 * intensity
114            scale_x = scale
115            scale_y = scale
116
117        elif wiggle_type == 'tail_wag':
118            # Like a wagging tail - base stays, tip moves
119            freq = cycles * 2 * math.pi
120            wag = math.sin(freq * t) * intensity
121
122            # Rotation focused at one end
123            rotation = wag * 20
124            offset_x = wag * 15
125
126        # Apply transformations
127        if object_type == 'emoji':
128            size = object_data['size']
129            size_x = int(size * scale_x)
130            size_y = int(size * scale_y)
131
132            # For non-uniform scaling or rotation, we need to use PIL transforms
133            if abs(scale_x - scale_y) > 0.01 or abs(rotation) > 0.1:
134                # Create emoji on transparent canvas
135                canvas_size = int(size * 2)
136                emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
137
138                # Draw emoji
139                draw_emoji_enhanced(
140                    emoji_canvas,
141                    emoji=object_data['emoji'],
142                    position=(canvas_size // 2 - size // 2, canvas_size // 2 - size // 2),
143                    size=size,
144                    shadow=False
145                )
146
147                # Scale
148                if abs(scale_x - scale_y) > 0.01:
149                    new_size = (int(canvas_size * scale_x), int(canvas_size * scale_y))
150                    emoji_canvas = emoji_canvas.resize(new_size, Image.LANCZOS)
151                    canvas_size_x, canvas_size_y = new_size
152                else:
153                    canvas_size_x = canvas_size_y = canvas_size
154
155                # Rotate
156                if abs(rotation) > 0.1:
157                    emoji_canvas = emoji_canvas.rotate(
158                        rotation,
159                        resample=Image.BICUBIC,
160                        expand=False
161                    )
162
163                # Position with offset
164                paste_x = int(center_pos[0] - canvas_size_x // 2 + offset_x)
165                paste_y = int(center_pos[1] - canvas_size_y // 2 + offset_y)
166
167                frame_rgba = frame.convert('RGBA')
168                frame_rgba.paste(emoji_canvas, (paste_x, paste_y), emoji_canvas)
169                frame = frame_rgba.convert('RGB')
170            else:
171                # Simple case - just offset
172                pos_x = int(center_pos[0] - size // 2 + offset_x)
173                pos_y = int(center_pos[1] - size // 2 + offset_y)
174                draw_emoji_enhanced(
175                    frame,
176                    emoji=object_data['emoji'],
177                    position=(pos_x, pos_y),
178                    size=size,
179                    shadow=object_data.get('shadow', True)
180                )
181
182        elif object_type == 'text':
183            from core.typography import draw_text_with_outline
184
185            # Create text on canvas for transformation
186            canvas_size = max(frame_width, frame_height)
187            text_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
188
189            # Convert to RGB for drawing
190            text_canvas_rgb = text_canvas.convert('RGB')
191            text_canvas_rgb.paste(bg_color, (0, 0, canvas_size, canvas_size))
192
193            draw_text_with_outline(
194                text_canvas_rgb,
195                text=object_data.get('text', 'WIGGLE'),
196                position=(canvas_size // 2, canvas_size // 2),
197                font_size=object_data.get('font_size', 50),
198                text_color=object_data.get('text_color', (0, 0, 0)),
199                outline_color=object_data.get('outline_color', (255, 255, 255)),
200                outline_width=3,
201                centered=True
202            )
203
204            # Make transparent
205            text_canvas = text_canvas_rgb.convert('RGBA')
206            data = text_canvas.getdata()
207            new_data = []
208            for item in data:
209                if item[:3] == bg_color:
210                    new_data.append((255, 255, 255, 0))
211                else:
212                    new_data.append(item)
213            text_canvas.putdata(new_data)
214
215            # Apply rotation
216            if abs(rotation) > 0.1:
217                text_canvas = text_canvas.rotate(rotation, center=(canvas_size // 2, canvas_size // 2), resample=Image.BICUBIC)
218
219            # Crop to frame with offset
220            left = (canvas_size - frame_width) // 2 - int(offset_x)
221            top = (canvas_size - frame_height) // 2 - int(offset_y)
222            text_cropped = text_canvas.crop((left, top, left + frame_width, top + frame_height))
223
224            frame_rgba = frame.convert('RGBA')
225            frame = Image.alpha_composite(frame_rgba, text_cropped)
226            frame = frame.convert('RGB')
227
228        frames.append(frame)
229
230    return frames
231
232
233def create_excited_wiggle(
234    emoji: str = '🎉',
235    num_frames: int = 20,
236    frame_size: int = 128
237) -> list[Image.Image]:
238    """
239    Create excited wiggle for emoji GIFs.
240
241    Args:
242        emoji: Emoji to wiggle
243        num_frames: Number of frames
244        frame_size: Frame size (square)
245
246    Returns:
247        List of frames
248    """
249    return create_wiggle_animation(
250        object_type='emoji',
251        object_data={'emoji': emoji, 'size': 80, 'shadow': False},
252        num_frames=num_frames,
253        wiggle_type='jello',
254        intensity=0.8,
255        cycles=2,
256        center_pos=(frame_size // 2, frame_size // 2),
257        frame_width=frame_size,
258        frame_height=frame_size,
259        bg_color=(255, 255, 255)
260    )
261
262
263# Example usage
264if __name__ == '__main__':
265    print("Creating wiggle animations...")
266
267    builder = GIFBuilder(width=480, height=480, fps=20)
268
269    # Example 1: Jello wiggle
270    frames = create_wiggle_animation(
271        object_type='emoji',
272        object_data={'emoji': '🎈', 'size': 100},
273        num_frames=40,
274        wiggle_type='jello',
275        intensity=1.0,
276        cycles=2
277    )
278    builder.add_frames(frames)
279    builder.save('wiggle_jello.gif', num_colors=128)
280
281    # Example 2: Wave
282    builder.clear()
283    frames = create_wiggle_animation(
284        object_type='emoji',
285        object_data={'emoji': '🌊', 'size': 100},
286        num_frames=30,
287        wiggle_type='wave',
288        intensity=1.2,
289        cycles=3
290    )
291    builder.add_frames(frames)
292    builder.save('wiggle_wave.gif', num_colors=128)
293
294    # Example 3: Excited wiggle (emoji size)
295    builder = GIFBuilder(width=128, height=128, fps=15)
296    frames = create_excited_wiggle(emoji='🎉', num_frames=20)
297    builder.add_frames(frames)
298    builder.save('wiggle_excited.gif', num_colors=48, optimize_for_emoji=True)
299
300    print("Created wiggle animations!")