332 lines
11 KiB
Python
332 lines
11 KiB
Python
#!/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!")
|