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