main
1#!/usr/bin/env python3
2"""
3Fade Animation - Fade in, fade out, and crossfade effects.
4
5Creates smooth opacity transitions for appearing, disappearing, and transitioning.
6"""
7
8import sys
9from pathlib import Path
10
11sys.path.append(str(Path(__file__).parent.parent))
12
13from PIL import Image, ImageDraw
14import numpy as np
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_fade_animation(
21 object_type: str = 'emoji',
22 object_data: dict | None = None,
23 num_frames: int = 30,
24 fade_type: str = 'in', # 'in', 'out', 'in_out', 'blink'
25 easing: str = 'ease_in_out',
26 center_pos: tuple[int, int] = (240, 240),
27 frame_width: int = 480,
28 frame_height: int = 480,
29 bg_color: tuple[int, int, int] = (255, 255, 255)
30) -> list[Image.Image]:
31 """
32 Create fade animation.
33
34 Args:
35 object_type: 'emoji', 'text', 'image'
36 object_data: Object configuration
37 num_frames: Number of frames
38 fade_type: Type of fade effect
39 easing: Easing function
40 center_pos: Center position
41 frame_width: Frame width
42 frame_height: Frame height
43 bg_color: Background color
44
45 Returns:
46 List of frames
47 """
48 frames = []
49
50 # Default object data
51 if object_data is None:
52 if object_type == 'emoji':
53 object_data = {'emoji': '✨', 'size': 100}
54
55 for i in range(num_frames):
56 t = i / (num_frames - 1) if num_frames > 1 else 0
57
58 # Calculate opacity based on fade type
59 if fade_type == 'in':
60 opacity = interpolate(0, 1, t, easing)
61 elif fade_type == 'out':
62 opacity = interpolate(1, 0, t, easing)
63 elif fade_type == 'in_out':
64 if t < 0.5:
65 opacity = interpolate(0, 1, t * 2, easing)
66 else:
67 opacity = interpolate(1, 0, (t - 0.5) * 2, easing)
68 elif fade_type == 'blink':
69 # Quick fade out and back in
70 if t < 0.2:
71 opacity = interpolate(1, 0, t / 0.2, 'ease_in')
72 elif t < 0.4:
73 opacity = interpolate(0, 1, (t - 0.2) / 0.2, 'ease_out')
74 else:
75 opacity = 1.0
76 else:
77 opacity = interpolate(0, 1, t, easing)
78
79 # Create background
80 frame_bg = create_blank_frame(frame_width, frame_height, bg_color)
81
82 # Create object layer with transparency
83 if object_type == 'emoji':
84 # Create RGBA canvas for emoji
85 emoji_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
86 emoji_size = object_data['size']
87 draw_emoji_enhanced(
88 emoji_canvas,
89 emoji=object_data['emoji'],
90 position=(center_pos[0] - emoji_size // 2, center_pos[1] - emoji_size // 2),
91 size=emoji_size,
92 shadow=object_data.get('shadow', False)
93 )
94
95 # Apply opacity
96 emoji_canvas = apply_opacity(emoji_canvas, opacity)
97
98 # Composite onto background
99 frame_bg_rgba = frame_bg.convert('RGBA')
100 frame = Image.alpha_composite(frame_bg_rgba, emoji_canvas)
101 frame = frame.convert('RGB')
102
103 elif object_type == 'text':
104 from core.typography import draw_text_with_outline
105
106 # Create text on separate layer
107 text_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
108 text_canvas_rgb = text_canvas.convert('RGB')
109 text_canvas_rgb.paste(bg_color, (0, 0, frame_width, frame_height))
110
111 draw_text_with_outline(
112 text_canvas_rgb,
113 text=object_data.get('text', 'FADE'),
114 position=center_pos,
115 font_size=object_data.get('font_size', 60),
116 text_color=object_data.get('text_color', (0, 0, 0)),
117 outline_color=object_data.get('outline_color', (255, 255, 255)),
118 outline_width=3,
119 centered=True
120 )
121
122 # Convert to RGBA and make background transparent
123 text_canvas = text_canvas_rgb.convert('RGBA')
124 data = text_canvas.getdata()
125 new_data = []
126 for item in data:
127 if item[:3] == bg_color:
128 new_data.append((255, 255, 255, 0))
129 else:
130 new_data.append(item)
131 text_canvas.putdata(new_data)
132
133 # Apply opacity
134 text_canvas = apply_opacity(text_canvas, opacity)
135
136 # Composite
137 frame_bg_rgba = frame_bg.convert('RGBA')
138 frame = Image.alpha_composite(frame_bg_rgba, text_canvas)
139 frame = frame.convert('RGB')
140
141 else:
142 frame = frame_bg
143
144 frames.append(frame)
145
146 return frames
147
148
149def apply_opacity(image: Image.Image, opacity: float) -> Image.Image:
150 """
151 Apply opacity to an RGBA image.
152
153 Args:
154 image: RGBA image
155 opacity: Opacity value (0.0 to 1.0)
156
157 Returns:
158 Image with adjusted opacity
159 """
160 if image.mode != 'RGBA':
161 image = image.convert('RGBA')
162
163 # Get alpha channel
164 r, g, b, a = image.split()
165
166 # Multiply alpha by opacity
167 a_array = np.array(a, dtype=np.float32)
168 a_array = a_array * opacity
169 a = Image.fromarray(a_array.astype(np.uint8))
170
171 # Merge back
172 return Image.merge('RGBA', (r, g, b, a))
173
174
175def create_crossfade(
176 object1_data: dict,
177 object2_data: dict,
178 num_frames: int = 30,
179 easing: str = 'ease_in_out',
180 object_type: str = 'emoji',
181 center_pos: tuple[int, int] = (240, 240),
182 frame_width: int = 480,
183 frame_height: int = 480,
184 bg_color: tuple[int, int, int] = (255, 255, 255)
185) -> list[Image.Image]:
186 """
187 Crossfade between two objects.
188
189 Args:
190 object1_data: First object configuration
191 object2_data: Second object configuration
192 num_frames: Number of frames
193 easing: Easing function
194 object_type: Type of objects
195 center_pos: Center position
196 frame_width: Frame width
197 frame_height: Frame height
198 bg_color: Background color
199
200 Returns:
201 List of frames
202 """
203 frames = []
204
205 for i in range(num_frames):
206 t = i / (num_frames - 1) if num_frames > 1 else 0
207
208 # Calculate opacities
209 opacity1 = interpolate(1, 0, t, easing)
210 opacity2 = interpolate(0, 1, t, easing)
211
212 # Create background
213 frame = create_blank_frame(frame_width, frame_height, bg_color)
214
215 if object_type == 'emoji':
216 # Create first emoji
217 emoji1_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
218 size1 = object1_data['size']
219 draw_emoji_enhanced(
220 emoji1_canvas,
221 emoji=object1_data['emoji'],
222 position=(center_pos[0] - size1 // 2, center_pos[1] - size1 // 2),
223 size=size1,
224 shadow=False
225 )
226 emoji1_canvas = apply_opacity(emoji1_canvas, opacity1)
227
228 # Create second emoji
229 emoji2_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
230 size2 = object2_data['size']
231 draw_emoji_enhanced(
232 emoji2_canvas,
233 emoji=object2_data['emoji'],
234 position=(center_pos[0] - size2 // 2, center_pos[1] - size2 // 2),
235 size=size2,
236 shadow=False
237 )
238 emoji2_canvas = apply_opacity(emoji2_canvas, opacity2)
239
240 # Composite both
241 frame_rgba = frame.convert('RGBA')
242 frame_rgba = Image.alpha_composite(frame_rgba, emoji1_canvas)
243 frame_rgba = Image.alpha_composite(frame_rgba, emoji2_canvas)
244 frame = frame_rgba.convert('RGB')
245
246 frames.append(frame)
247
248 return frames
249
250
251def create_fade_to_color(
252 start_color: tuple[int, int, int],
253 end_color: tuple[int, int, int],
254 num_frames: int = 20,
255 easing: str = 'linear',
256 frame_width: int = 480,
257 frame_height: int = 480
258) -> list[Image.Image]:
259 """
260 Fade from one solid color to another.
261
262 Args:
263 start_color: Starting RGB color
264 end_color: Ending RGB color
265 num_frames: Number of frames
266 easing: Easing function
267 frame_width: Frame width
268 frame_height: Frame height
269
270 Returns:
271 List of frames
272 """
273 frames = []
274
275 for i in range(num_frames):
276 t = i / (num_frames - 1) if num_frames > 1 else 0
277
278 # Interpolate each color channel
279 r = int(interpolate(start_color[0], end_color[0], t, easing))
280 g = int(interpolate(start_color[1], end_color[1], t, easing))
281 b = int(interpolate(start_color[2], end_color[2], t, easing))
282
283 color = (r, g, b)
284 frame = create_blank_frame(frame_width, frame_height, color)
285 frames.append(frame)
286
287 return frames
288
289
290# Example usage
291if __name__ == '__main__':
292 print("Creating fade animations...")
293
294 builder = GIFBuilder(width=480, height=480, fps=20)
295
296 # Example 1: Fade in
297 frames = create_fade_animation(
298 object_type='emoji',
299 object_data={'emoji': '✨', 'size': 120},
300 num_frames=30,
301 fade_type='in',
302 easing='ease_out'
303 )
304 builder.add_frames(frames)
305 builder.save('fade_in.gif', num_colors=128)
306
307 # Example 2: Crossfade
308 builder.clear()
309 frames = create_crossfade(
310 object1_data={'emoji': '😊', 'size': 100},
311 object2_data={'emoji': '😂', 'size': 100},
312 num_frames=30,
313 object_type='emoji'
314 )
315 builder.add_frames(frames)
316 builder.save('fade_crossfade.gif', num_colors=128)
317
318 # Example 3: Blink
319 builder.clear()
320 frames = create_fade_animation(
321 object_type='emoji',
322 object_data={'emoji': '👀', 'size': 100},
323 num_frames=20,
324 fade_type='blink'
325 )
326 builder.add_frames(frames)
327 builder.save('fade_blink.gif', num_colors=128)
328
329 print("Created fade animations!")