313 lines
9.9 KiB
Python
313 lines
9.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Zoom Animation - Scale objects dramatically for emphasis.
|
|
|
|
Creates zoom in, zoom out, and dramatic scaling effects.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
import math
|
|
|
|
sys.path.append(str(Path(__file__).parent.parent))
|
|
|
|
from PIL import Image, ImageFilter
|
|
from core.gif_builder import GIFBuilder
|
|
from core.frame_composer import create_blank_frame, draw_emoji_enhanced
|
|
from core.easing import interpolate
|
|
|
|
|
|
def create_zoom_animation(
|
|
object_type: str = 'emoji',
|
|
object_data: dict | None = None,
|
|
num_frames: int = 30,
|
|
zoom_type: str = 'in', # 'in', 'out', 'in_out', 'punch'
|
|
scale_range: tuple[float, float] = (0.1, 2.0),
|
|
easing: str = 'ease_out',
|
|
add_motion_blur: bool = False,
|
|
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 zoom animation.
|
|
|
|
Args:
|
|
object_type: 'emoji', 'text', 'image'
|
|
object_data: Object configuration
|
|
num_frames: Number of frames
|
|
zoom_type: Type of zoom effect
|
|
scale_range: (start_scale, end_scale) tuple
|
|
easing: Easing function
|
|
add_motion_blur: Add blur for speed effect
|
|
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}
|
|
|
|
base_size = object_data.get('size', 100) if object_type == 'emoji' else object_data.get('font_size', 60)
|
|
start_scale, end_scale = scale_range
|
|
|
|
for i in range(num_frames):
|
|
t = i / (num_frames - 1) if num_frames > 1 else 0
|
|
|
|
# Calculate scale based on zoom type
|
|
if zoom_type == 'in':
|
|
scale = interpolate(start_scale, end_scale, t, easing)
|
|
elif zoom_type == 'out':
|
|
scale = interpolate(end_scale, start_scale, t, easing)
|
|
elif zoom_type == 'in_out':
|
|
if t < 0.5:
|
|
scale = interpolate(start_scale, end_scale, t * 2, easing)
|
|
else:
|
|
scale = interpolate(end_scale, start_scale, (t - 0.5) * 2, easing)
|
|
elif zoom_type == 'punch':
|
|
# Quick zoom in with overshoot then settle
|
|
if t < 0.3:
|
|
scale = interpolate(start_scale, end_scale * 1.2, t / 0.3, 'ease_out')
|
|
else:
|
|
scale = interpolate(end_scale * 1.2, end_scale, (t - 0.3) / 0.7, 'elastic_out')
|
|
else:
|
|
scale = interpolate(start_scale, end_scale, t, easing)
|
|
|
|
# Create frame
|
|
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
|
|
if object_type == 'emoji':
|
|
current_size = int(base_size * scale)
|
|
|
|
# Clamp size to reasonable bounds
|
|
current_size = max(12, min(current_size, frame_width * 2))
|
|
|
|
# Create emoji on transparent background
|
|
canvas_size = max(frame_width, frame_height, current_size) * 2
|
|
emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
|
|
|
draw_emoji_enhanced(
|
|
emoji_canvas,
|
|
emoji=object_data['emoji'],
|
|
position=(canvas_size // 2 - current_size // 2, canvas_size // 2 - current_size // 2),
|
|
size=current_size,
|
|
shadow=False
|
|
)
|
|
|
|
# Optional motion blur for fast zooms
|
|
if add_motion_blur and abs(scale - 1.0) > 0.5:
|
|
blur_amount = min(5, int(abs(scale - 1.0) * 3))
|
|
emoji_canvas = emoji_canvas.filter(ImageFilter.GaussianBlur(blur_amount))
|
|
|
|
# Crop to frame size centered
|
|
left = (canvas_size - frame_width) // 2
|
|
top = (canvas_size - frame_height) // 2
|
|
emoji_cropped = emoji_canvas.crop((left, top, left + frame_width, top + frame_height))
|
|
|
|
# Composite
|
|
frame_rgba = frame.convert('RGBA')
|
|
frame = Image.alpha_composite(frame_rgba, emoji_cropped)
|
|
frame = frame.convert('RGB')
|
|
|
|
elif object_type == 'text':
|
|
from core.typography import draw_text_with_outline
|
|
|
|
current_size = int(base_size * scale)
|
|
current_size = max(10, min(current_size, 500))
|
|
|
|
# Create oversized canvas for large text
|
|
canvas_size = max(frame_width, frame_height, current_size * 10)
|
|
text_canvas = Image.new('RGB', (canvas_size, canvas_size), bg_color)
|
|
|
|
draw_text_with_outline(
|
|
text_canvas,
|
|
text=object_data.get('text', 'ZOOM'),
|
|
position=(canvas_size // 2, canvas_size // 2),
|
|
font_size=current_size,
|
|
text_color=object_data.get('text_color', (0, 0, 0)),
|
|
outline_color=object_data.get('outline_color', (255, 255, 255)),
|
|
outline_width=max(2, int(current_size * 0.05)),
|
|
centered=True
|
|
)
|
|
|
|
# Crop to frame
|
|
left = (canvas_size - frame_width) // 2
|
|
top = (canvas_size - frame_height) // 2
|
|
frame = text_canvas.crop((left, top, left + frame_width, top + frame_height))
|
|
|
|
frames.append(frame)
|
|
|
|
return frames
|
|
|
|
|
|
def create_explosion_zoom(
|
|
emoji: str = '💥',
|
|
num_frames: int = 20,
|
|
frame_width: int = 480,
|
|
frame_height: int = 480,
|
|
bg_color: tuple[int, int, int] = (255, 255, 255)
|
|
) -> list[Image.Image]:
|
|
"""
|
|
Create dramatic explosion zoom effect.
|
|
|
|
Args:
|
|
emoji: Emoji to explode
|
|
num_frames: Number of frames
|
|
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
|
|
|
|
# Exponential zoom
|
|
scale = 0.1 * math.exp(t * 5)
|
|
|
|
# Add rotation for drama
|
|
angle = t * 360 * 2
|
|
|
|
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
|
|
current_size = int(100 * scale)
|
|
current_size = max(12, min(current_size, frame_width * 3))
|
|
|
|
# Create emoji
|
|
canvas_size = max(frame_width, frame_height, current_size) * 2
|
|
emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
|
|
|
draw_emoji_enhanced(
|
|
emoji_canvas,
|
|
emoji=emoji,
|
|
position=(canvas_size // 2 - current_size // 2, canvas_size // 2 - current_size // 2),
|
|
size=current_size,
|
|
shadow=False
|
|
)
|
|
|
|
# Rotate
|
|
emoji_canvas = emoji_canvas.rotate(angle, center=(canvas_size // 2, canvas_size // 2), resample=Image.BICUBIC)
|
|
|
|
# Add motion blur for later frames
|
|
if t > 0.5:
|
|
blur_amount = int((t - 0.5) * 10)
|
|
emoji_canvas = emoji_canvas.filter(ImageFilter.GaussianBlur(blur_amount))
|
|
|
|
# Crop and composite
|
|
left = (canvas_size - frame_width) // 2
|
|
top = (canvas_size - frame_height) // 2
|
|
emoji_cropped = emoji_canvas.crop((left, top, left + frame_width, top + frame_height))
|
|
|
|
frame_rgba = frame.convert('RGBA')
|
|
frame = Image.alpha_composite(frame_rgba, emoji_cropped)
|
|
frame = frame.convert('RGB')
|
|
|
|
frames.append(frame)
|
|
|
|
return frames
|
|
|
|
|
|
def create_mind_blown_zoom(
|
|
emoji: str = '🤯',
|
|
num_frames: int = 30,
|
|
frame_width: int = 480,
|
|
frame_height: int = 480,
|
|
bg_color: tuple[int, int, int] = (255, 255, 255)
|
|
) -> list[Image.Image]:
|
|
"""
|
|
Create "mind blown" dramatic zoom with shake.
|
|
|
|
Args:
|
|
emoji: Emoji to use
|
|
num_frames: Number of frames
|
|
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
|
|
|
|
# Zoom in then shake
|
|
if t < 0.5:
|
|
scale = interpolate(0.3, 1.2, t * 2, 'ease_out')
|
|
shake_x = 0
|
|
shake_y = 0
|
|
else:
|
|
scale = 1.2
|
|
# Shake intensifies
|
|
shake_intensity = (t - 0.5) * 40
|
|
shake_x = int(math.sin(t * 50) * shake_intensity)
|
|
shake_y = int(math.cos(t * 45) * shake_intensity)
|
|
|
|
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
|
|
current_size = int(100 * scale)
|
|
center_x = frame_width // 2 + shake_x
|
|
center_y = frame_height // 2 + shake_y
|
|
|
|
emoji_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
|
draw_emoji_enhanced(
|
|
emoji_canvas,
|
|
emoji=emoji,
|
|
position=(center_x - current_size // 2, center_y - current_size // 2),
|
|
size=current_size,
|
|
shadow=False
|
|
)
|
|
|
|
frame_rgba = frame.convert('RGBA')
|
|
frame = Image.alpha_composite(frame_rgba, emoji_canvas)
|
|
frame = frame.convert('RGB')
|
|
|
|
frames.append(frame)
|
|
|
|
return frames
|
|
|
|
|
|
# Example usage
|
|
if __name__ == '__main__':
|
|
print("Creating zoom animations...")
|
|
|
|
builder = GIFBuilder(width=480, height=480, fps=20)
|
|
|
|
# Example 1: Zoom in
|
|
frames = create_zoom_animation(
|
|
object_type='emoji',
|
|
object_data={'emoji': '🔍', 'size': 100},
|
|
num_frames=30,
|
|
zoom_type='in',
|
|
scale_range=(0.1, 1.5),
|
|
easing='ease_out'
|
|
)
|
|
builder.add_frames(frames)
|
|
builder.save('zoom_in.gif', num_colors=128)
|
|
|
|
# Example 2: Explosion zoom
|
|
builder.clear()
|
|
frames = create_explosion_zoom(emoji='💥', num_frames=20)
|
|
builder.add_frames(frames)
|
|
builder.save('zoom_explosion.gif', num_colors=128)
|
|
|
|
# Example 3: Mind blown
|
|
builder.clear()
|
|
frames = create_mind_blown_zoom(emoji='🤯', num_frames=30)
|
|
builder.add_frames(frames)
|
|
builder.save('zoom_mind_blown.gif', num_colors=128)
|
|
|
|
print("Created zoom animations!")
|