Files
gh-bkootaz-https-github-com…/skills/slack-gif-creator/templates/explode.py
2025-11-29 18:01:21 +08:00

332 lines
11 KiB
Python
Executable File

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