main
1#!/usr/bin/env python3
2"""
3Zoom Animation - Scale objects dramatically for emphasis.
4
5Creates zoom in, zoom out, and dramatic scaling effects.
6"""
7
8import sys
9from pathlib import Path
10import math
11
12sys.path.append(str(Path(__file__).parent.parent))
13
14from PIL import Image, ImageFilter
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_zoom_animation(
21 object_type: str = 'emoji',
22 object_data: dict | None = None,
23 num_frames: int = 30,
24 zoom_type: str = 'in', # 'in', 'out', 'in_out', 'punch'
25 scale_range: tuple[float, float] = (0.1, 2.0),
26 easing: str = 'ease_out',
27 add_motion_blur: bool = False,
28 center_pos: tuple[int, int] = (240, 240),
29 frame_width: int = 480,
30 frame_height: int = 480,
31 bg_color: tuple[int, int, int] = (255, 255, 255)
32) -> list[Image.Image]:
33 """
34 Create zoom animation.
35
36 Args:
37 object_type: 'emoji', 'text', 'image'
38 object_data: Object configuration
39 num_frames: Number of frames
40 zoom_type: Type of zoom effect
41 scale_range: (start_scale, end_scale) tuple
42 easing: Easing function
43 add_motion_blur: Add blur for speed effect
44 center_pos: Center position
45 frame_width: Frame width
46 frame_height: Frame height
47 bg_color: Background color
48
49 Returns:
50 List of frames
51 """
52 frames = []
53
54 # Default object data
55 if object_data is None:
56 if object_type == 'emoji':
57 object_data = {'emoji': '🔍', 'size': 100}
58
59 base_size = object_data.get('size', 100) if object_type == 'emoji' else object_data.get('font_size', 60)
60 start_scale, end_scale = scale_range
61
62 for i in range(num_frames):
63 t = i / (num_frames - 1) if num_frames > 1 else 0
64
65 # Calculate scale based on zoom type
66 if zoom_type == 'in':
67 scale = interpolate(start_scale, end_scale, t, easing)
68 elif zoom_type == 'out':
69 scale = interpolate(end_scale, start_scale, t, easing)
70 elif zoom_type == 'in_out':
71 if t < 0.5:
72 scale = interpolate(start_scale, end_scale, t * 2, easing)
73 else:
74 scale = interpolate(end_scale, start_scale, (t - 0.5) * 2, easing)
75 elif zoom_type == 'punch':
76 # Quick zoom in with overshoot then settle
77 if t < 0.3:
78 scale = interpolate(start_scale, end_scale * 1.2, t / 0.3, 'ease_out')
79 else:
80 scale = interpolate(end_scale * 1.2, end_scale, (t - 0.3) / 0.7, 'elastic_out')
81 else:
82 scale = interpolate(start_scale, end_scale, t, easing)
83
84 # Create frame
85 frame = create_blank_frame(frame_width, frame_height, bg_color)
86
87 if object_type == 'emoji':
88 current_size = int(base_size * scale)
89
90 # Clamp size to reasonable bounds
91 current_size = max(12, min(current_size, frame_width * 2))
92
93 # Create emoji on transparent background
94 canvas_size = max(frame_width, frame_height, current_size) * 2
95 emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
96
97 draw_emoji_enhanced(
98 emoji_canvas,
99 emoji=object_data['emoji'],
100 position=(canvas_size // 2 - current_size // 2, canvas_size // 2 - current_size // 2),
101 size=current_size,
102 shadow=False
103 )
104
105 # Optional motion blur for fast zooms
106 if add_motion_blur and abs(scale - 1.0) > 0.5:
107 blur_amount = min(5, int(abs(scale - 1.0) * 3))
108 emoji_canvas = emoji_canvas.filter(ImageFilter.GaussianBlur(blur_amount))
109
110 # Crop to frame size centered
111 left = (canvas_size - frame_width) // 2
112 top = (canvas_size - frame_height) // 2
113 emoji_cropped = emoji_canvas.crop((left, top, left + frame_width, top + frame_height))
114
115 # Composite
116 frame_rgba = frame.convert('RGBA')
117 frame = Image.alpha_composite(frame_rgba, emoji_cropped)
118 frame = frame.convert('RGB')
119
120 elif object_type == 'text':
121 from core.typography import draw_text_with_outline
122
123 current_size = int(base_size * scale)
124 current_size = max(10, min(current_size, 500))
125
126 # Create oversized canvas for large text
127 canvas_size = max(frame_width, frame_height, current_size * 10)
128 text_canvas = Image.new('RGB', (canvas_size, canvas_size), bg_color)
129
130 draw_text_with_outline(
131 text_canvas,
132 text=object_data.get('text', 'ZOOM'),
133 position=(canvas_size // 2, canvas_size // 2),
134 font_size=current_size,
135 text_color=object_data.get('text_color', (0, 0, 0)),
136 outline_color=object_data.get('outline_color', (255, 255, 255)),
137 outline_width=max(2, int(current_size * 0.05)),
138 centered=True
139 )
140
141 # Crop to frame
142 left = (canvas_size - frame_width) // 2
143 top = (canvas_size - frame_height) // 2
144 frame = text_canvas.crop((left, top, left + frame_width, top + frame_height))
145
146 frames.append(frame)
147
148 return frames
149
150
151def create_explosion_zoom(
152 emoji: str = '💥',
153 num_frames: int = 20,
154 frame_width: int = 480,
155 frame_height: int = 480,
156 bg_color: tuple[int, int, int] = (255, 255, 255)
157) -> list[Image.Image]:
158 """
159 Create dramatic explosion zoom effect.
160
161 Args:
162 emoji: Emoji to explode
163 num_frames: Number of frames
164 frame_width: Frame width
165 frame_height: Frame height
166 bg_color: Background color
167
168 Returns:
169 List of frames
170 """
171 frames = []
172
173 for i in range(num_frames):
174 t = i / (num_frames - 1) if num_frames > 1 else 0
175
176 # Exponential zoom
177 scale = 0.1 * math.exp(t * 5)
178
179 # Add rotation for drama
180 angle = t * 360 * 2
181
182 frame = create_blank_frame(frame_width, frame_height, bg_color)
183
184 current_size = int(100 * scale)
185 current_size = max(12, min(current_size, frame_width * 3))
186
187 # Create emoji
188 canvas_size = max(frame_width, frame_height, current_size) * 2
189 emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
190
191 draw_emoji_enhanced(
192 emoji_canvas,
193 emoji=emoji,
194 position=(canvas_size // 2 - current_size // 2, canvas_size // 2 - current_size // 2),
195 size=current_size,
196 shadow=False
197 )
198
199 # Rotate
200 emoji_canvas = emoji_canvas.rotate(angle, center=(canvas_size // 2, canvas_size // 2), resample=Image.BICUBIC)
201
202 # Add motion blur for later frames
203 if t > 0.5:
204 blur_amount = int((t - 0.5) * 10)
205 emoji_canvas = emoji_canvas.filter(ImageFilter.GaussianBlur(blur_amount))
206
207 # Crop and composite
208 left = (canvas_size - frame_width) // 2
209 top = (canvas_size - frame_height) // 2
210 emoji_cropped = emoji_canvas.crop((left, top, left + frame_width, top + frame_height))
211
212 frame_rgba = frame.convert('RGBA')
213 frame = Image.alpha_composite(frame_rgba, emoji_cropped)
214 frame = frame.convert('RGB')
215
216 frames.append(frame)
217
218 return frames
219
220
221def create_mind_blown_zoom(
222 emoji: str = '🤯',
223 num_frames: int = 30,
224 frame_width: int = 480,
225 frame_height: int = 480,
226 bg_color: tuple[int, int, int] = (255, 255, 255)
227) -> list[Image.Image]:
228 """
229 Create "mind blown" dramatic zoom with shake.
230
231 Args:
232 emoji: Emoji to use
233 num_frames: Number of frames
234 frame_width: Frame width
235 frame_height: Frame height
236 bg_color: Background color
237
238 Returns:
239 List of frames
240 """
241 frames = []
242
243 for i in range(num_frames):
244 t = i / (num_frames - 1) if num_frames > 1 else 0
245
246 # Zoom in then shake
247 if t < 0.5:
248 scale = interpolate(0.3, 1.2, t * 2, 'ease_out')
249 shake_x = 0
250 shake_y = 0
251 else:
252 scale = 1.2
253 # Shake intensifies
254 shake_intensity = (t - 0.5) * 40
255 shake_x = int(math.sin(t * 50) * shake_intensity)
256 shake_y = int(math.cos(t * 45) * shake_intensity)
257
258 frame = create_blank_frame(frame_width, frame_height, bg_color)
259
260 current_size = int(100 * scale)
261 center_x = frame_width // 2 + shake_x
262 center_y = frame_height // 2 + shake_y
263
264 emoji_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
265 draw_emoji_enhanced(
266 emoji_canvas,
267 emoji=emoji,
268 position=(center_x - current_size // 2, center_y - current_size // 2),
269 size=current_size,
270 shadow=False
271 )
272
273 frame_rgba = frame.convert('RGBA')
274 frame = Image.alpha_composite(frame_rgba, emoji_canvas)
275 frame = frame.convert('RGB')
276
277 frames.append(frame)
278
279 return frames
280
281
282# Example usage
283if __name__ == '__main__':
284 print("Creating zoom animations...")
285
286 builder = GIFBuilder(width=480, height=480, fps=20)
287
288 # Example 1: Zoom in
289 frames = create_zoom_animation(
290 object_type='emoji',
291 object_data={'emoji': '🔍', 'size': 100},
292 num_frames=30,
293 zoom_type='in',
294 scale_range=(0.1, 1.5),
295 easing='ease_out'
296 )
297 builder.add_frames(frames)
298 builder.save('zoom_in.gif', num_colors=128)
299
300 # Example 2: Explosion zoom
301 builder.clear()
302 frames = create_explosion_zoom(emoji='💥', num_frames=20)
303 builder.add_frames(frames)
304 builder.save('zoom_explosion.gif', num_colors=128)
305
306 # Example 3: Mind blown
307 builder.clear()
308 frames = create_mind_blown_zoom(emoji='🤯', num_frames=30)
309 builder.add_frames(frames)
310 builder.save('zoom_mind_blown.gif', num_colors=128)
311
312 print("Created zoom animations!")