1#!/usr/bin/env python3
  2"""
  3Morph Animation - Transform between different emojis or shapes.
  4
  5Creates smooth transitions and transformations.
  6"""
  7
  8import sys
  9from pathlib import Path
 10
 11sys.path.append(str(Path(__file__).parent.parent))
 12
 13from PIL import Image
 14import numpy as np
 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_morph_animation(
 21    object1_data: dict,
 22    object2_data: dict,
 23    num_frames: int = 30,
 24    morph_type: str = 'crossfade',  # 'crossfade', 'scale', 'spin_morph'
 25    easing: str = 'ease_in_out',
 26    object_type: str = 'emoji',
 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 morphing animation between two objects.
 34
 35    Args:
 36        object1_data: First object configuration
 37        object2_data: Second object configuration
 38        num_frames: Number of frames
 39        morph_type: Type of morph effect
 40        easing: Easing function
 41        object_type: Type of objects
 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    for i in range(num_frames):
 53        t = i / (num_frames - 1) if num_frames > 1 else 0
 54        frame = create_blank_frame(frame_width, frame_height, bg_color)
 55
 56        if morph_type == 'crossfade':
 57            # Simple crossfade between two objects
 58            opacity1 = interpolate(1, 0, t, easing)
 59            opacity2 = interpolate(0, 1, t, easing)
 60
 61            if object_type == 'emoji':
 62                # Create first emoji
 63                emoji1_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
 64                size1 = object1_data['size']
 65                draw_emoji_enhanced(
 66                    emoji1_canvas,
 67                    emoji=object1_data['emoji'],
 68                    position=(center_pos[0] - size1 // 2, center_pos[1] - size1 // 2),
 69                    size=size1,
 70                    shadow=False
 71                )
 72
 73                # Apply opacity
 74                from templates.fade import apply_opacity
 75                emoji1_canvas = apply_opacity(emoji1_canvas, opacity1)
 76
 77                # Create second emoji
 78                emoji2_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
 79                size2 = object2_data['size']
 80                draw_emoji_enhanced(
 81                    emoji2_canvas,
 82                    emoji=object2_data['emoji'],
 83                    position=(center_pos[0] - size2 // 2, center_pos[1] - size2 // 2),
 84                    size=size2,
 85                    shadow=False
 86                )
 87
 88                emoji2_canvas = apply_opacity(emoji2_canvas, opacity2)
 89
 90                # Composite both
 91                frame_rgba = frame.convert('RGBA')
 92                frame_rgba = Image.alpha_composite(frame_rgba, emoji1_canvas)
 93                frame_rgba = Image.alpha_composite(frame_rgba, emoji2_canvas)
 94                frame = frame_rgba.convert('RGB')
 95
 96            elif object_type == 'circle':
 97                # Morph between two circles
 98                radius1 = object1_data['radius']
 99                radius2 = object2_data['radius']
100                color1 = object1_data['color']
101                color2 = object2_data['color']
102
103                # Interpolate properties
104                current_radius = int(interpolate(radius1, radius2, t, easing))
105                current_color = tuple(
106                    int(interpolate(color1[i], color2[i], t, easing))
107                    for i in range(3)
108                )
109
110                draw_circle(frame, center_pos, current_radius, fill_color=current_color)
111
112        elif morph_type == 'scale':
113            # First object scales down as second scales up
114            if object_type == 'emoji':
115                scale1 = interpolate(1.0, 0.0, t, easing)
116                scale2 = interpolate(0.0, 1.0, t, easing)
117
118                # Draw first emoji (shrinking)
119                if scale1 > 0.05:
120                    size1 = int(object1_data['size'] * scale1)
121                    size1 = max(12, size1)
122                    emoji1_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
123                    draw_emoji_enhanced(
124                        emoji1_canvas,
125                        emoji=object1_data['emoji'],
126                        position=(center_pos[0] - size1 // 2, center_pos[1] - size1 // 2),
127                        size=size1,
128                        shadow=False
129                    )
130
131                    frame_rgba = frame.convert('RGBA')
132                    frame = Image.alpha_composite(frame_rgba, emoji1_canvas)
133                    frame = frame.convert('RGB')
134
135                # Draw second emoji (growing)
136                if scale2 > 0.05:
137                    size2 = int(object2_data['size'] * scale2)
138                    size2 = max(12, size2)
139                    emoji2_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
140                    draw_emoji_enhanced(
141                        emoji2_canvas,
142                        emoji=object2_data['emoji'],
143                        position=(center_pos[0] - size2 // 2, center_pos[1] - size2 // 2),
144                        size=size2,
145                        shadow=False
146                    )
147
148                    frame_rgba = frame.convert('RGBA')
149                    frame = Image.alpha_composite(frame_rgba, emoji2_canvas)
150                    frame = frame.convert('RGB')
151
152        elif morph_type == 'spin_morph':
153            # Spin while morphing (flip-like)
154            import math
155
156            # Calculate rotation (0 to 180 degrees)
157            angle = interpolate(0, 180, t, easing)
158            scale_factor = abs(math.cos(math.radians(angle)))
159
160            # Determine which object to show
161            if angle < 90:
162                current_object = object1_data
163            else:
164                current_object = object2_data
165
166            # Skip when edge-on
167            if scale_factor < 0.05:
168                frames.append(frame)
169                continue
170
171            if object_type == 'emoji':
172                size = current_object['size']
173                canvas_size = size * 2
174                emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
175
176                draw_emoji_enhanced(
177                    emoji_canvas,
178                    emoji=current_object['emoji'],
179                    position=(canvas_size // 2 - size // 2, canvas_size // 2 - size // 2),
180                    size=size,
181                    shadow=False
182                )
183
184                # Scale horizontally for spin effect
185                new_width = max(1, int(canvas_size * scale_factor))
186                emoji_scaled = emoji_canvas.resize((new_width, canvas_size), Image.LANCZOS)
187
188                paste_x = center_pos[0] - new_width // 2
189                paste_y = center_pos[1] - canvas_size // 2
190
191                frame_rgba = frame.convert('RGBA')
192                frame_rgba.paste(emoji_scaled, (paste_x, paste_y), emoji_scaled)
193                frame = frame_rgba.convert('RGB')
194
195        frames.append(frame)
196
197    return frames
198
199
200def create_reaction_morph(
201    emoji_start: str,
202    emoji_end: str,
203    num_frames: int = 20,
204    frame_size: int = 128
205) -> list[Image.Image]:
206    """
207    Create quick emoji reaction morph (for emoji GIFs).
208
209    Args:
210        emoji_start: Starting emoji
211        emoji_end: Ending emoji
212        num_frames: Number of frames
213        frame_size: Frame size (square)
214
215    Returns:
216        List of frames
217    """
218    return create_morph_animation(
219        object1_data={'emoji': emoji_start, 'size': 80},
220        object2_data={'emoji': emoji_end, 'size': 80},
221        num_frames=num_frames,
222        morph_type='crossfade',
223        easing='ease_in_out',
224        object_type='emoji',
225        center_pos=(frame_size // 2, frame_size // 2),
226        frame_width=frame_size,
227        frame_height=frame_size,
228        bg_color=(255, 255, 255)
229    )
230
231
232def create_shape_morph(
233    shapes: list[dict],
234    num_frames: int = 60,
235    frames_per_shape: int = 20,
236    frame_width: int = 480,
237    frame_height: int = 480,
238    bg_color: tuple[int, int, int] = (255, 255, 255)
239) -> list[Image.Image]:
240    """
241    Morph through a sequence of shapes.
242
243    Args:
244        shapes: List of shape dicts with 'radius' and 'color'
245        num_frames: Total number of frames
246        frames_per_shape: Frames to spend on each morph
247        frame_width: Frame width
248        frame_height: Frame height
249        bg_color: Background color
250
251    Returns:
252        List of frames
253    """
254    frames = []
255    center = (frame_width // 2, frame_height // 2)
256
257    for i in range(num_frames):
258        # Determine which shapes we're morphing between
259        cycle_progress = (i % (frames_per_shape * len(shapes))) / frames_per_shape
260        shape_idx = int(cycle_progress) % len(shapes)
261        next_shape_idx = (shape_idx + 1) % len(shapes)
262
263        # Progress between these two shapes
264        t = cycle_progress - shape_idx
265
266        shape1 = shapes[shape_idx]
267        shape2 = shapes[next_shape_idx]
268
269        # Interpolate properties
270        radius = int(interpolate(shape1['radius'], shape2['radius'], t, 'ease_in_out'))
271        color = tuple(
272            int(interpolate(shape1['color'][j], shape2['color'][j], t, 'ease_in_out'))
273            for j in range(3)
274        )
275
276        # Draw frame
277        frame = create_blank_frame(frame_width, frame_height, bg_color)
278        draw_circle(frame, center, radius, fill_color=color)
279
280        frames.append(frame)
281
282    return frames
283
284
285# Example usage
286if __name__ == '__main__':
287    print("Creating morph animations...")
288
289    builder = GIFBuilder(width=480, height=480, fps=20)
290
291    # Example 1: Crossfade morph
292    frames = create_morph_animation(
293        object1_data={'emoji': '😊', 'size': 100},
294        object2_data={'emoji': '😂', 'size': 100},
295        num_frames=30,
296        morph_type='crossfade',
297        object_type='emoji'
298    )
299    builder.add_frames(frames)
300    builder.save('morph_crossfade.gif', num_colors=128)
301
302    # Example 2: Scale morph
303    builder.clear()
304    frames = create_morph_animation(
305        object1_data={'emoji': '🌙', 'size': 100},
306        object2_data={'emoji': '☀️', 'size': 100},
307        num_frames=40,
308        morph_type='scale',
309        object_type='emoji'
310    )
311    builder.add_frames(frames)
312    builder.save('morph_scale.gif', num_colors=128)
313
314    # Example 3: Shape morph cycle
315    builder.clear()
316    from core.color_palettes import get_palette
317    palette = get_palette('vibrant')
318
319    shapes = [
320        {'radius': 60, 'color': palette['primary']},
321        {'radius': 80, 'color': palette['secondary']},
322        {'radius': 50, 'color': palette['accent']},
323        {'radius': 70, 'color': palette['success']}
324    ]
325    frames = create_shape_morph(shapes, num_frames=80, frames_per_shape=20)
326    builder.add_frames(frames)
327    builder.save('morph_shapes.gif', num_colors=64)
328
329    print("Created morph animations!")