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