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