#!/usr/bin/env python3 """ Explode Animation - Break objects into pieces that fly outward. Creates explosion, shatter, and particle burst effects. """ import sys from pathlib import Path import math import random sys.path.append(str(Path(__file__).parent.parent)) from PIL import Image, ImageDraw import numpy as np from core.gif_builder import GIFBuilder from core.frame_composer import create_blank_frame, draw_emoji_enhanced from core.visual_effects import ParticleSystem from core.easing import interpolate def create_explode_animation( object_type: str = 'emoji', object_data: dict | None = None, num_frames: int = 30, explode_type: str = 'burst', # 'burst', 'shatter', 'dissolve', 'implode' num_pieces: int = 20, explosion_speed: float = 5.0, center_pos: tuple[int, int] = (240, 240), frame_width: int = 480, frame_height: int = 480, bg_color: tuple[int, int, int] = (255, 255, 255) ) -> list[Image.Image]: """ Create explosion animation. Args: object_type: 'emoji', 'circle', 'text' object_data: Object configuration num_frames: Number of frames explode_type: Type of explosion num_pieces: Number of pieces/particles explosion_speed: Speed of explosion center_pos: Center position frame_width: Frame width frame_height: Frame height bg_color: Background color Returns: List of frames """ frames = [] # Default object data if object_data is None: if object_type == 'emoji': object_data = {'emoji': '💣', 'size': 100} # Generate pieces/particles pieces = [] for _ in range(num_pieces): angle = random.uniform(0, 2 * math.pi) speed = random.uniform(explosion_speed * 0.5, explosion_speed * 1.5) vx = math.cos(angle) * speed vy = math.sin(angle) * speed size = random.randint(3, 12) color = ( random.randint(100, 255), random.randint(100, 255), random.randint(100, 255) ) rotation_speed = random.uniform(-20, 20) pieces.append({ 'vx': vx, 'vy': vy, 'size': size, 'color': color, 'rotation': 0, 'rotation_speed': rotation_speed }) for i in range(num_frames): t = i / (num_frames - 1) if num_frames > 1 else 0 frame = create_blank_frame(frame_width, frame_height, bg_color) draw = ImageDraw.Draw(frame) if explode_type == 'burst': # Show object at start, then explode if t < 0.2: # Object still intact scale = interpolate(1.0, 1.2, t / 0.2, 'ease_out') if object_type == 'emoji': size = int(object_data['size'] * scale) draw_emoji_enhanced( frame, emoji=object_data['emoji'], position=(center_pos[0] - size // 2, center_pos[1] - size // 2), size=size, shadow=False ) else: # Exploded - draw pieces explosion_t = (t - 0.2) / 0.8 for piece in pieces: # Update position x = center_pos[0] + piece['vx'] * explosion_t * 50 y = center_pos[1] + piece['vy'] * explosion_t * 50 + 0.5 * 300 * explosion_t ** 2 # Gravity # Fade out alpha = 1.0 - explosion_t if alpha > 0: color = tuple(int(c * alpha) for c in piece['color']) size = int(piece['size'] * (1 - explosion_t * 0.5)) draw.ellipse( [x - size, y - size, x + size, y + size], fill=color ) elif explode_type == 'shatter': # Break into geometric pieces if t < 0.15: # Object intact if object_type == 'emoji': draw_emoji_enhanced( frame, emoji=object_data['emoji'], position=(center_pos[0] - object_data['size'] // 2, center_pos[1] - object_data['size'] // 2), size=object_data['size'], shadow=False ) else: # Shattered shatter_t = (t - 0.15) / 0.85 # Draw triangular shards for piece in pieces[:min(10, len(pieces))]: x = center_pos[0] + piece['vx'] * shatter_t * 30 y = center_pos[1] + piece['vy'] * shatter_t * 30 + 0.5 * 200 * shatter_t ** 2 # Update rotation rotation = piece['rotation_speed'] * shatter_t * 100 # Draw triangle shard shard_size = piece['size'] * 2 points = [] for j in range(3): angle = (rotation + j * 120) * math.pi / 180 px = x + shard_size * math.cos(angle) py = y + shard_size * math.sin(angle) points.append((px, py)) alpha = 1.0 - shatter_t if alpha > 0: color = tuple(int(c * alpha) for c in piece['color']) draw.polygon(points, fill=color) elif explode_type == 'dissolve': # Dissolve into particles dissolve_scale = interpolate(1.0, 0.0, t, 'ease_in') if dissolve_scale > 0.1: # Draw fading object if object_type == 'emoji': size = int(object_data['size'] * dissolve_scale) size = max(12, size) emoji_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0)) draw_emoji_enhanced( emoji_canvas, emoji=object_data['emoji'], position=(center_pos[0] - size // 2, center_pos[1] - size // 2), size=size, shadow=False ) # Apply opacity from templates.fade import apply_opacity emoji_canvas = apply_opacity(emoji_canvas, dissolve_scale) frame_rgba = frame.convert('RGBA') frame = Image.alpha_composite(frame_rgba, emoji_canvas) frame = frame.convert('RGB') draw = ImageDraw.Draw(frame) # Draw outward-moving particles for piece in pieces: x = center_pos[0] + piece['vx'] * t * 40 y = center_pos[1] + piece['vy'] * t * 40 alpha = 1.0 - t if alpha > 0: color = tuple(int(c * alpha) for c in piece['color']) size = int(piece['size'] * (1 - t * 0.5)) draw.ellipse( [x - size, y - size, x + size, y + size], fill=color ) elif explode_type == 'implode': # Reverse explosion - pieces fly inward if t < 0.7: # Pieces converging implode_t = 1.0 - (t / 0.7) for piece in pieces: x = center_pos[0] + piece['vx'] * implode_t * 50 y = center_pos[1] + piece['vy'] * implode_t * 50 alpha = 1.0 - (1.0 - implode_t) * 0.5 color = tuple(int(c * alpha) for c in piece['color']) size = int(piece['size'] * alpha) draw.ellipse( [x - size, y - size, x + size, y + size], fill=color ) else: # Object reforms reform_t = (t - 0.7) / 0.3 scale = interpolate(0.5, 1.0, reform_t, 'elastic_out') if object_type == 'emoji': size = int(object_data['size'] * scale) draw_emoji_enhanced( frame, emoji=object_data['emoji'], position=(center_pos[0] - size // 2, center_pos[1] - size // 2), size=size, shadow=False ) frames.append(frame) return frames def create_particle_burst( num_frames: int = 25, particle_count: int = 30, center_pos: tuple[int, int] = (240, 240), colors: list[tuple[int, int, int]] | None = None, frame_width: int = 480, frame_height: int = 480, bg_color: tuple[int, int, int] = (255, 255, 255) ) -> list[Image.Image]: """ Create simple particle burst effect. Args: num_frames: Number of frames particle_count: Number of particles center_pos: Burst center colors: Particle colors (None for random) frame_width: Frame width frame_height: Frame height bg_color: Background color Returns: List of frames """ particles = ParticleSystem() # Emit particles if colors is None: from core.color_palettes import get_palette palette = get_palette('vibrant') colors = [palette['primary'], palette['secondary'], palette['accent']] for _ in range(particle_count): color = random.choice(colors) particles.emit( center_pos[0], center_pos[1], count=1, speed=random.uniform(3, 8), color=color, lifetime=random.uniform(20, 30), size=random.randint(3, 8), shape='star' ) frames = [] for _ in range(num_frames): frame = create_blank_frame(frame_width, frame_height, bg_color) particles.update() particles.render(frame) frames.append(frame) return frames # Example usage if __name__ == '__main__': print("Creating explode animations...") builder = GIFBuilder(width=480, height=480, fps=20) # Example 1: Burst frames = create_explode_animation( object_type='emoji', object_data={'emoji': '💣', 'size': 100}, num_frames=30, explode_type='burst', num_pieces=25 ) builder.add_frames(frames) builder.save('explode_burst.gif', num_colors=128) # Example 2: Shatter builder.clear() frames = create_explode_animation( object_type='emoji', object_data={'emoji': '🪟', 'size': 100}, num_frames=30, explode_type='shatter', num_pieces=12 ) builder.add_frames(frames) builder.save('explode_shatter.gif', num_colors=128) # Example 3: Particle burst builder.clear() frames = create_particle_burst(num_frames=25, particle_count=40) builder.add_frames(frames) builder.save('explode_particles.gif', num_colors=128) print("Created explode animations!")