main
1#!/usr/bin/env python3
2"""
3Explode Animation - Break objects into pieces that fly outward.
4
5Creates explosion, shatter, and particle burst effects.
6"""
7
8import sys
9from pathlib import Path
10import math
11import random
12
13sys.path.append(str(Path(__file__).parent.parent))
14
15from PIL import Image, ImageDraw
16import numpy as np
17from core.gif_builder import GIFBuilder
18from core.frame_composer import create_blank_frame, draw_emoji_enhanced
19from core.visual_effects import ParticleSystem
20from core.easing import interpolate
21
22
23def create_explode_animation(
24 object_type: str = 'emoji',
25 object_data: dict | None = None,
26 num_frames: int = 30,
27 explode_type: str = 'burst', # 'burst', 'shatter', 'dissolve', 'implode'
28 num_pieces: int = 20,
29 explosion_speed: float = 5.0,
30 center_pos: tuple[int, int] = (240, 240),
31 frame_width: int = 480,
32 frame_height: int = 480,
33 bg_color: tuple[int, int, int] = (255, 255, 255)
34) -> list[Image.Image]:
35 """
36 Create explosion animation.
37
38 Args:
39 object_type: 'emoji', 'circle', 'text'
40 object_data: Object configuration
41 num_frames: Number of frames
42 explode_type: Type of explosion
43 num_pieces: Number of pieces/particles
44 explosion_speed: Speed of explosion
45 center_pos: Center position
46 frame_width: Frame width
47 frame_height: Frame height
48 bg_color: Background color
49
50 Returns:
51 List of frames
52 """
53 frames = []
54
55 # Default object data
56 if object_data is None:
57 if object_type == 'emoji':
58 object_data = {'emoji': '💣', 'size': 100}
59
60 # Generate pieces/particles
61 pieces = []
62 for _ in range(num_pieces):
63 angle = random.uniform(0, 2 * math.pi)
64 speed = random.uniform(explosion_speed * 0.5, explosion_speed * 1.5)
65 vx = math.cos(angle) * speed
66 vy = math.sin(angle) * speed
67 size = random.randint(3, 12)
68 color = (
69 random.randint(100, 255),
70 random.randint(100, 255),
71 random.randint(100, 255)
72 )
73 rotation_speed = random.uniform(-20, 20)
74
75 pieces.append({
76 'vx': vx,
77 'vy': vy,
78 'size': size,
79 'color': color,
80 'rotation': 0,
81 'rotation_speed': rotation_speed
82 })
83
84 for i in range(num_frames):
85 t = i / (num_frames - 1) if num_frames > 1 else 0
86 frame = create_blank_frame(frame_width, frame_height, bg_color)
87 draw = ImageDraw.Draw(frame)
88
89 if explode_type == 'burst':
90 # Show object at start, then explode
91 if t < 0.2:
92 # Object still intact
93 scale = interpolate(1.0, 1.2, t / 0.2, 'ease_out')
94 if object_type == 'emoji':
95 size = int(object_data['size'] * scale)
96 draw_emoji_enhanced(
97 frame,
98 emoji=object_data['emoji'],
99 position=(center_pos[0] - size // 2, center_pos[1] - size // 2),
100 size=size,
101 shadow=False
102 )
103 else:
104 # Exploded - draw pieces
105 explosion_t = (t - 0.2) / 0.8
106 for piece in pieces:
107 # Update position
108 x = center_pos[0] + piece['vx'] * explosion_t * 50
109 y = center_pos[1] + piece['vy'] * explosion_t * 50 + 0.5 * 300 * explosion_t ** 2 # Gravity
110
111 # Fade out
112 alpha = 1.0 - explosion_t
113 if alpha > 0:
114 color = tuple(int(c * alpha) for c in piece['color'])
115 size = int(piece['size'] * (1 - explosion_t * 0.5))
116
117 draw.ellipse(
118 [x - size, y - size, x + size, y + size],
119 fill=color
120 )
121
122 elif explode_type == 'shatter':
123 # Break into geometric pieces
124 if t < 0.15:
125 # Object intact
126 if object_type == 'emoji':
127 draw_emoji_enhanced(
128 frame,
129 emoji=object_data['emoji'],
130 position=(center_pos[0] - object_data['size'] // 2,
131 center_pos[1] - object_data['size'] // 2),
132 size=object_data['size'],
133 shadow=False
134 )
135 else:
136 # Shattered
137 shatter_t = (t - 0.15) / 0.85
138
139 # Draw triangular shards
140 for piece in pieces[:min(10, len(pieces))]:
141 x = center_pos[0] + piece['vx'] * shatter_t * 30
142 y = center_pos[1] + piece['vy'] * shatter_t * 30 + 0.5 * 200 * shatter_t ** 2
143
144 # Update rotation
145 rotation = piece['rotation_speed'] * shatter_t * 100
146
147 # Draw triangle shard
148 shard_size = piece['size'] * 2
149 points = []
150 for j in range(3):
151 angle = (rotation + j * 120) * math.pi / 180
152 px = x + shard_size * math.cos(angle)
153 py = y + shard_size * math.sin(angle)
154 points.append((px, py))
155
156 alpha = 1.0 - shatter_t
157 if alpha > 0:
158 color = tuple(int(c * alpha) for c in piece['color'])
159 draw.polygon(points, fill=color)
160
161 elif explode_type == 'dissolve':
162 # Dissolve into particles
163 dissolve_scale = interpolate(1.0, 0.0, t, 'ease_in')
164
165 if dissolve_scale > 0.1:
166 # Draw fading object
167 if object_type == 'emoji':
168 size = int(object_data['size'] * dissolve_scale)
169 size = max(12, size)
170
171 emoji_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
172 draw_emoji_enhanced(
173 emoji_canvas,
174 emoji=object_data['emoji'],
175 position=(center_pos[0] - size // 2, center_pos[1] - size // 2),
176 size=size,
177 shadow=False
178 )
179
180 # Apply opacity
181 from templates.fade import apply_opacity
182 emoji_canvas = apply_opacity(emoji_canvas, dissolve_scale)
183
184 frame_rgba = frame.convert('RGBA')
185 frame = Image.alpha_composite(frame_rgba, emoji_canvas)
186 frame = frame.convert('RGB')
187 draw = ImageDraw.Draw(frame)
188
189 # Draw outward-moving particles
190 for piece in pieces:
191 x = center_pos[0] + piece['vx'] * t * 40
192 y = center_pos[1] + piece['vy'] * t * 40
193
194 alpha = 1.0 - t
195 if alpha > 0:
196 color = tuple(int(c * alpha) for c in piece['color'])
197 size = int(piece['size'] * (1 - t * 0.5))
198 draw.ellipse(
199 [x - size, y - size, x + size, y + size],
200 fill=color
201 )
202
203 elif explode_type == 'implode':
204 # Reverse explosion - pieces fly inward
205 if t < 0.7:
206 # Pieces converging
207 implode_t = 1.0 - (t / 0.7)
208 for piece in pieces:
209 x = center_pos[0] + piece['vx'] * implode_t * 50
210 y = center_pos[1] + piece['vy'] * implode_t * 50
211
212 alpha = 1.0 - (1.0 - implode_t) * 0.5
213 color = tuple(int(c * alpha) for c in piece['color'])
214 size = int(piece['size'] * alpha)
215
216 draw.ellipse(
217 [x - size, y - size, x + size, y + size],
218 fill=color
219 )
220 else:
221 # Object reforms
222 reform_t = (t - 0.7) / 0.3
223 scale = interpolate(0.5, 1.0, reform_t, 'elastic_out')
224
225 if object_type == 'emoji':
226 size = int(object_data['size'] * scale)
227 draw_emoji_enhanced(
228 frame,
229 emoji=object_data['emoji'],
230 position=(center_pos[0] - size // 2, center_pos[1] - size // 2),
231 size=size,
232 shadow=False
233 )
234
235 frames.append(frame)
236
237 return frames
238
239
240def create_particle_burst(
241 num_frames: int = 25,
242 particle_count: int = 30,
243 center_pos: tuple[int, int] = (240, 240),
244 colors: list[tuple[int, int, int]] | None = None,
245 frame_width: int = 480,
246 frame_height: int = 480,
247 bg_color: tuple[int, int, int] = (255, 255, 255)
248) -> list[Image.Image]:
249 """
250 Create simple particle burst effect.
251
252 Args:
253 num_frames: Number of frames
254 particle_count: Number of particles
255 center_pos: Burst center
256 colors: Particle colors (None for random)
257 frame_width: Frame width
258 frame_height: Frame height
259 bg_color: Background color
260
261 Returns:
262 List of frames
263 """
264 particles = ParticleSystem()
265
266 # Emit particles
267 if colors is None:
268 from core.color_palettes import get_palette
269 palette = get_palette('vibrant')
270 colors = [palette['primary'], palette['secondary'], palette['accent']]
271
272 for _ in range(particle_count):
273 color = random.choice(colors)
274 particles.emit(
275 center_pos[0], center_pos[1],
276 count=1,
277 speed=random.uniform(3, 8),
278 color=color,
279 lifetime=random.uniform(20, 30),
280 size=random.randint(3, 8),
281 shape='star'
282 )
283
284 frames = []
285 for _ in range(num_frames):
286 frame = create_blank_frame(frame_width, frame_height, bg_color)
287
288 particles.update()
289 particles.render(frame)
290
291 frames.append(frame)
292
293 return frames
294
295
296# Example usage
297if __name__ == '__main__':
298 print("Creating explode animations...")
299
300 builder = GIFBuilder(width=480, height=480, fps=20)
301
302 # Example 1: Burst
303 frames = create_explode_animation(
304 object_type='emoji',
305 object_data={'emoji': '💣', 'size': 100},
306 num_frames=30,
307 explode_type='burst',
308 num_pieces=25
309 )
310 builder.add_frames(frames)
311 builder.save('explode_burst.gif', num_colors=128)
312
313 # Example 2: Shatter
314 builder.clear()
315 frames = create_explode_animation(
316 object_type='emoji',
317 object_data={'emoji': '🪟', 'size': 100},
318 num_frames=30,
319 explode_type='shatter',
320 num_pieces=12
321 )
322 builder.add_frames(frames)
323 builder.save('explode_shatter.gif', num_colors=128)
324
325 # Example 3: Particle burst
326 builder.clear()
327 frames = create_particle_burst(num_frames=25, particle_count=40)
328 builder.add_frames(frames)
329 builder.save('explode_particles.gif', num_colors=128)
330
331 print("Created explode animations!")