Files
2025-11-30 08:32:12 +08:00

330 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Morph Animation - Transform between different emojis or shapes.
Creates smooth transitions and transformations.
"""
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).parent.parent))
from PIL import Image
import numpy as np
from core.gif_builder import GIFBuilder
from core.frame_composer import create_blank_frame, draw_emoji_enhanced, draw_circle
from core.easing import interpolate
def create_morph_animation(
object1_data: dict,
object2_data: dict,
num_frames: int = 30,
morph_type: str = 'crossfade', # 'crossfade', 'scale', 'spin_morph'
easing: str = 'ease_in_out',
object_type: str = 'emoji',
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 morphing animation between two objects.
Args:
object1_data: First object configuration
object2_data: Second object configuration
num_frames: Number of frames
morph_type: Type of morph effect
easing: Easing function
object_type: Type of objects
center_pos: Center position
frame_width: Frame width
frame_height: Frame height
bg_color: Background color
Returns:
List of frames
"""
frames = []
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)
if morph_type == 'crossfade':
# Simple crossfade between two objects
opacity1 = interpolate(1, 0, t, easing)
opacity2 = interpolate(0, 1, t, easing)
if object_type == 'emoji':
# Create first emoji
emoji1_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
size1 = object1_data['size']
draw_emoji_enhanced(
emoji1_canvas,
emoji=object1_data['emoji'],
position=(center_pos[0] - size1 // 2, center_pos[1] - size1 // 2),
size=size1,
shadow=False
)
# Apply opacity
from templates.fade import apply_opacity
emoji1_canvas = apply_opacity(emoji1_canvas, opacity1)
# Create second emoji
emoji2_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
size2 = object2_data['size']
draw_emoji_enhanced(
emoji2_canvas,
emoji=object2_data['emoji'],
position=(center_pos[0] - size2 // 2, center_pos[1] - size2 // 2),
size=size2,
shadow=False
)
emoji2_canvas = apply_opacity(emoji2_canvas, opacity2)
# Composite both
frame_rgba = frame.convert('RGBA')
frame_rgba = Image.alpha_composite(frame_rgba, emoji1_canvas)
frame_rgba = Image.alpha_composite(frame_rgba, emoji2_canvas)
frame = frame_rgba.convert('RGB')
elif object_type == 'circle':
# Morph between two circles
radius1 = object1_data['radius']
radius2 = object2_data['radius']
color1 = object1_data['color']
color2 = object2_data['color']
# Interpolate properties
current_radius = int(interpolate(radius1, radius2, t, easing))
current_color = tuple(
int(interpolate(color1[i], color2[i], t, easing))
for i in range(3)
)
draw_circle(frame, center_pos, current_radius, fill_color=current_color)
elif morph_type == 'scale':
# First object scales down as second scales up
if object_type == 'emoji':
scale1 = interpolate(1.0, 0.0, t, easing)
scale2 = interpolate(0.0, 1.0, t, easing)
# Draw first emoji (shrinking)
if scale1 > 0.05:
size1 = int(object1_data['size'] * scale1)
size1 = max(12, size1)
emoji1_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
draw_emoji_enhanced(
emoji1_canvas,
emoji=object1_data['emoji'],
position=(center_pos[0] - size1 // 2, center_pos[1] - size1 // 2),
size=size1,
shadow=False
)
frame_rgba = frame.convert('RGBA')
frame = Image.alpha_composite(frame_rgba, emoji1_canvas)
frame = frame.convert('RGB')
# Draw second emoji (growing)
if scale2 > 0.05:
size2 = int(object2_data['size'] * scale2)
size2 = max(12, size2)
emoji2_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
draw_emoji_enhanced(
emoji2_canvas,
emoji=object2_data['emoji'],
position=(center_pos[0] - size2 // 2, center_pos[1] - size2 // 2),
size=size2,
shadow=False
)
frame_rgba = frame.convert('RGBA')
frame = Image.alpha_composite(frame_rgba, emoji2_canvas)
frame = frame.convert('RGB')
elif morph_type == 'spin_morph':
# Spin while morphing (flip-like)
import math
# Calculate rotation (0 to 180 degrees)
angle = interpolate(0, 180, t, easing)
scale_factor = abs(math.cos(math.radians(angle)))
# Determine which object to show
if angle < 90:
current_object = object1_data
else:
current_object = object2_data
# Skip when edge-on
if scale_factor < 0.05:
frames.append(frame)
continue
if object_type == 'emoji':
size = current_object['size']
canvas_size = size * 2
emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
draw_emoji_enhanced(
emoji_canvas,
emoji=current_object['emoji'],
position=(canvas_size // 2 - size // 2, canvas_size // 2 - size // 2),
size=size,
shadow=False
)
# Scale horizontally for spin effect
new_width = max(1, int(canvas_size * scale_factor))
emoji_scaled = emoji_canvas.resize((new_width, canvas_size), Image.LANCZOS)
paste_x = center_pos[0] - new_width // 2
paste_y = center_pos[1] - canvas_size // 2
frame_rgba = frame.convert('RGBA')
frame_rgba.paste(emoji_scaled, (paste_x, paste_y), emoji_scaled)
frame = frame_rgba.convert('RGB')
frames.append(frame)
return frames
def create_reaction_morph(
emoji_start: str,
emoji_end: str,
num_frames: int = 20,
frame_size: int = 128
) -> list[Image.Image]:
"""
Create quick emoji reaction morph (for emoji GIFs).
Args:
emoji_start: Starting emoji
emoji_end: Ending emoji
num_frames: Number of frames
frame_size: Frame size (square)
Returns:
List of frames
"""
return create_morph_animation(
object1_data={'emoji': emoji_start, 'size': 80},
object2_data={'emoji': emoji_end, 'size': 80},
num_frames=num_frames,
morph_type='crossfade',
easing='ease_in_out',
object_type='emoji',
center_pos=(frame_size // 2, frame_size // 2),
frame_width=frame_size,
frame_height=frame_size,
bg_color=(255, 255, 255)
)
def create_shape_morph(
shapes: list[dict],
num_frames: int = 60,
frames_per_shape: int = 20,
frame_width: int = 480,
frame_height: int = 480,
bg_color: tuple[int, int, int] = (255, 255, 255)
) -> list[Image.Image]:
"""
Morph through a sequence of shapes.
Args:
shapes: List of shape dicts with 'radius' and 'color'
num_frames: Total number of frames
frames_per_shape: Frames to spend on each morph
frame_width: Frame width
frame_height: Frame height
bg_color: Background color
Returns:
List of frames
"""
frames = []
center = (frame_width // 2, frame_height // 2)
for i in range(num_frames):
# Determine which shapes we're morphing between
cycle_progress = (i % (frames_per_shape * len(shapes))) / frames_per_shape
shape_idx = int(cycle_progress) % len(shapes)
next_shape_idx = (shape_idx + 1) % len(shapes)
# Progress between these two shapes
t = cycle_progress - shape_idx
shape1 = shapes[shape_idx]
shape2 = shapes[next_shape_idx]
# Interpolate properties
radius = int(interpolate(shape1['radius'], shape2['radius'], t, 'ease_in_out'))
color = tuple(
int(interpolate(shape1['color'][j], shape2['color'][j], t, 'ease_in_out'))
for j in range(3)
)
# Draw frame
frame = create_blank_frame(frame_width, frame_height, bg_color)
draw_circle(frame, center, radius, fill_color=color)
frames.append(frame)
return frames
# Example usage
if __name__ == '__main__':
print("Creating morph animations...")
builder = GIFBuilder(width=480, height=480, fps=20)
# Example 1: Crossfade morph
frames = create_morph_animation(
object1_data={'emoji': '😊', 'size': 100},
object2_data={'emoji': '😂', 'size': 100},
num_frames=30,
morph_type='crossfade',
object_type='emoji'
)
builder.add_frames(frames)
builder.save('morph_crossfade.gif', num_colors=128)
# Example 2: Scale morph
builder.clear()
frames = create_morph_animation(
object1_data={'emoji': '🌙', 'size': 100},
object2_data={'emoji': '☀️', 'size': 100},
num_frames=40,
morph_type='scale',
object_type='emoji'
)
builder.add_frames(frames)
builder.save('morph_scale.gif', num_colors=128)
# Example 3: Shape morph cycle
builder.clear()
from core.color_palettes import get_palette
palette = get_palette('vibrant')
shapes = [
{'radius': 60, 'color': palette['primary']},
{'radius': 80, 'color': palette['secondary']},
{'radius': 50, 'color': palette['accent']},
{'radius': 70, 'color': palette['success']}
]
frames = create_shape_morph(shapes, num_frames=80, frames_per_shape=20)
builder.add_frames(frames)
builder.save('morph_shapes.gif', num_colors=64)
print("Created morph animations!")