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