1#!/usr/bin/env python3
  2"""
  3Flip Animation - 3D-style card flip and rotation effects.
  4
  5Creates horizontal and vertical flips with perspective.
  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_flip_animation(
 21    object1_data: dict,
 22    object2_data: dict | None = None,
 23    num_frames: int = 30,
 24    flip_axis: str = 'horizontal',  # 'horizontal', 'vertical'
 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 3D-style flip animation.
 34
 35    Args:
 36        object1_data: First object (front side)
 37        object2_data: Second object (back side, None = same as front)
 38        num_frames: Number of frames
 39        flip_axis: Axis to flip around
 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    if object2_data is None:
 53        object2_data = object1_data
 54
 55    for i in range(num_frames):
 56        t = i / (num_frames - 1) if num_frames > 1 else 0
 57        frame = create_blank_frame(frame_width, frame_height, bg_color)
 58
 59        # Calculate rotation angle (0 to 180 degrees)
 60        angle = interpolate(0, 180, t, easing)
 61
 62        # Determine which side is visible and calculate scale
 63        if angle < 90:
 64            # Front side visible
 65            current_object = object1_data
 66            scale_factor = math.cos(math.radians(angle))
 67        else:
 68            # Back side visible
 69            current_object = object2_data
 70            scale_factor = abs(math.cos(math.radians(angle)))
 71
 72        # Don't draw when edge-on (very thin)
 73        if scale_factor < 0.05:
 74            frames.append(frame)
 75            continue
 76
 77        if object_type == 'emoji':
 78            size = current_object['size']
 79
 80            # Create emoji on canvas
 81            canvas_size = size * 2
 82            emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
 83
 84            draw_emoji_enhanced(
 85                emoji_canvas,
 86                emoji=current_object['emoji'],
 87                position=(canvas_size // 2 - size // 2, canvas_size // 2 - size // 2),
 88                size=size,
 89                shadow=False
 90            )
 91
 92            # Apply flip scaling
 93            if flip_axis == 'horizontal':
 94                # Scale horizontally for horizontal flip
 95                new_width = max(1, int(canvas_size * scale_factor))
 96                new_height = canvas_size
 97            else:
 98                # Scale vertically for vertical flip
 99                new_width = canvas_size
100                new_height = max(1, int(canvas_size * scale_factor))
101
102            # Resize to simulate 3D rotation
103            emoji_scaled = emoji_canvas.resize((new_width, new_height), Image.LANCZOS)
104
105            # Position centered
106            paste_x = center_pos[0] - new_width // 2
107            paste_y = center_pos[1] - new_height // 2
108
109            # Composite onto frame
110            frame_rgba = frame.convert('RGBA')
111            frame_rgba.paste(emoji_scaled, (paste_x, paste_y), emoji_scaled)
112            frame = frame_rgba.convert('RGB')
113
114        elif object_type == 'text':
115            from core.typography import draw_text_with_outline
116
117            # Create text on canvas
118            text = current_object.get('text', 'FLIP')
119            font_size = current_object.get('font_size', 50)
120
121            canvas_size = max(frame_width, frame_height)
122            text_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
123
124            # Draw on RGB for text rendering
125            text_canvas_rgb = text_canvas.convert('RGB')
126            text_canvas_rgb.paste(bg_color, (0, 0, canvas_size, canvas_size))
127
128            draw_text_with_outline(
129                text_canvas_rgb,
130                text=text,
131                position=(canvas_size // 2, canvas_size // 2),
132                font_size=font_size,
133                text_color=current_object.get('text_color', (0, 0, 0)),
134                outline_color=current_object.get('outline_color', (255, 255, 255)),
135                outline_width=3,
136                centered=True
137            )
138
139            # Make background transparent
140            text_canvas = text_canvas_rgb.convert('RGBA')
141            data = text_canvas.getdata()
142            new_data = []
143            for item in data:
144                if item[:3] == bg_color:
145                    new_data.append((255, 255, 255, 0))
146                else:
147                    new_data.append(item)
148            text_canvas.putdata(new_data)
149
150            # Apply flip scaling
151            if flip_axis == 'horizontal':
152                new_width = max(1, int(canvas_size * scale_factor))
153                new_height = canvas_size
154            else:
155                new_width = canvas_size
156                new_height = max(1, int(canvas_size * scale_factor))
157
158            text_scaled = text_canvas.resize((new_width, new_height), Image.LANCZOS)
159
160            # Center and crop
161            if flip_axis == 'horizontal':
162                left = (new_width - frame_width) // 2 if new_width > frame_width else 0
163                top = (canvas_size - frame_height) // 2
164                paste_x = center_pos[0] - min(new_width, frame_width) // 2
165                paste_y = 0
166
167                text_cropped = text_scaled.crop((
168                    left,
169                    top,
170                    left + min(new_width, frame_width),
171                    top + frame_height
172                ))
173            else:
174                left = (canvas_size - frame_width) // 2
175                top = (new_height - frame_height) // 2 if new_height > frame_height else 0
176                paste_x = 0
177                paste_y = center_pos[1] - min(new_height, frame_height) // 2
178
179                text_cropped = text_scaled.crop((
180                    left,
181                    top,
182                    left + frame_width,
183                    top + min(new_height, frame_height)
184                ))
185
186            frame_rgba = frame.convert('RGBA')
187            frame_rgba.paste(text_cropped, (paste_x, paste_y), text_cropped)
188            frame = frame_rgba.convert('RGB')
189
190        frames.append(frame)
191
192    return frames
193
194
195def create_quick_flip(
196    emoji_front: str,
197    emoji_back: str,
198    num_frames: int = 20,
199    frame_size: int = 128
200) -> list[Image.Image]:
201    """
202    Create quick flip for emoji GIFs.
203
204    Args:
205        emoji_front: Front emoji
206        emoji_back: Back emoji
207        num_frames: Number of frames
208        frame_size: Frame size (square)
209
210    Returns:
211        List of frames
212    """
213    return create_flip_animation(
214        object1_data={'emoji': emoji_front, 'size': 80},
215        object2_data={'emoji': emoji_back, 'size': 80},
216        num_frames=num_frames,
217        flip_axis='horizontal',
218        easing='ease_in_out',
219        object_type='emoji',
220        center_pos=(frame_size // 2, frame_size // 2),
221        frame_width=frame_size,
222        frame_height=frame_size,
223        bg_color=(255, 255, 255)
224    )
225
226
227def create_nope_flip(
228    num_frames: int = 25,
229    frame_width: int = 480,
230    frame_height: int = 480
231) -> list[Image.Image]:
232    """
233    Create "nope" reaction flip (like flipping table).
234
235    Args:
236        num_frames: Number of frames
237        frame_width: Frame width
238        frame_height: Frame height
239
240    Returns:
241        List of frames
242    """
243    return create_flip_animation(
244        object1_data={'text': 'NOPE', 'font_size': 80, 'text_color': (255, 50, 50)},
245        object2_data={'text': 'NOPE', 'font_size': 80, 'text_color': (255, 50, 50)},
246        num_frames=num_frames,
247        flip_axis='horizontal',
248        easing='ease_out',
249        object_type='text',
250        frame_width=frame_width,
251        frame_height=frame_height,
252        bg_color=(255, 255, 255)
253    )
254
255
256# Example usage
257if __name__ == '__main__':
258    print("Creating flip animations...")
259
260    builder = GIFBuilder(width=480, height=480, fps=20)
261
262    # Example 1: Emoji flip
263    frames = create_flip_animation(
264        object1_data={'emoji': '😊', 'size': 120},
265        object2_data={'emoji': '😂', 'size': 120},
266        num_frames=30,
267        flip_axis='horizontal',
268        object_type='emoji'
269    )
270    builder.add_frames(frames)
271    builder.save('flip_emoji.gif', num_colors=128)
272
273    # Example 2: Text flip
274    builder.clear()
275    frames = create_flip_animation(
276        object1_data={'text': 'YES', 'font_size': 80, 'text_color': (100, 200, 100)},
277        object2_data={'text': 'NO', 'font_size': 80, 'text_color': (200, 100, 100)},
278        num_frames=30,
279        flip_axis='vertical',
280        object_type='text'
281    )
282    builder.add_frames(frames)
283    builder.save('flip_text.gif', num_colors=128)
284
285    # Example 3: Quick flip (emoji size)
286    builder = GIFBuilder(width=128, height=128, fps=15)
287    frames = create_quick_flip('👍', '👎', num_frames=20)
288    builder.add_frames(frames)
289    builder.save('flip_quick.gif', num_colors=48, optimize_for_emoji=True)
290
291    print("Created flip animations!")