Initial commit
This commit is contained in:
293
skills/slack-gif-creator/templates/move.py
Executable file
293
skills/slack-gif-creator/templates/move.py
Executable file
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Move Animation - Move objects along paths with various motion types.
|
||||
|
||||
Provides flexible movement primitives for objects along linear, arc, or custom paths.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import math
|
||||
|
||||
sys.path.append(str(Path(__file__).parent.parent))
|
||||
|
||||
from core.gif_builder import GIFBuilder
|
||||
from core.frame_composer import create_blank_frame, draw_circle, draw_emoji_enhanced
|
||||
from core.easing import interpolate, calculate_arc_motion
|
||||
|
||||
|
||||
def create_move_animation(
|
||||
object_type: str = 'emoji',
|
||||
object_data: dict | None = None,
|
||||
start_pos: tuple[int, int] = (50, 240),
|
||||
end_pos: tuple[int, int] = (430, 240),
|
||||
num_frames: int = 30,
|
||||
motion_type: str = 'linear', # 'linear', 'arc', 'bezier', 'circle', 'wave'
|
||||
easing: str = 'ease_out',
|
||||
motion_params: dict | None = None,
|
||||
frame_width: int = 480,
|
||||
frame_height: int = 480,
|
||||
bg_color: tuple[int, int, int] = (255, 255, 255)
|
||||
) -> list:
|
||||
"""
|
||||
Create frames showing object moving along a path.
|
||||
|
||||
Args:
|
||||
object_type: 'circle', 'emoji', or 'custom'
|
||||
object_data: Data for the object
|
||||
start_pos: Starting (x, y) position
|
||||
end_pos: Ending (x, y) position
|
||||
num_frames: Number of frames
|
||||
motion_type: Type of motion path
|
||||
easing: Easing function name
|
||||
motion_params: Additional parameters for motion (e.g., {'arc_height': 100})
|
||||
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 == 'circle':
|
||||
object_data = {'radius': 30, 'color': (100, 150, 255)}
|
||||
elif object_type == 'emoji':
|
||||
object_data = {'emoji': '🚀', 'size': 60}
|
||||
|
||||
# Default motion params
|
||||
if motion_params is None:
|
||||
motion_params = {}
|
||||
|
||||
for i in range(num_frames):
|
||||
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
||||
|
||||
t = i / (num_frames - 1) if num_frames > 1 else 0
|
||||
|
||||
# Calculate position based on motion type
|
||||
if motion_type == 'linear':
|
||||
# Straight line with easing
|
||||
x = interpolate(start_pos[0], end_pos[0], t, easing)
|
||||
y = interpolate(start_pos[1], end_pos[1], t, easing)
|
||||
|
||||
elif motion_type == 'arc':
|
||||
# Parabolic arc
|
||||
arc_height = motion_params.get('arc_height', 100)
|
||||
x, y = calculate_arc_motion(start_pos, end_pos, arc_height, t)
|
||||
|
||||
elif motion_type == 'circle':
|
||||
# Circular motion around a center
|
||||
center = motion_params.get('center', (frame_width // 2, frame_height // 2))
|
||||
radius = motion_params.get('radius', 150)
|
||||
start_angle = motion_params.get('start_angle', 0)
|
||||
angle_range = motion_params.get('angle_range', 360) # Full circle
|
||||
|
||||
angle = start_angle + (angle_range * t)
|
||||
angle_rad = math.radians(angle)
|
||||
|
||||
x = center[0] + radius * math.cos(angle_rad)
|
||||
y = center[1] + radius * math.sin(angle_rad)
|
||||
|
||||
elif motion_type == 'wave':
|
||||
# Move in straight line but add wave motion
|
||||
wave_amplitude = motion_params.get('wave_amplitude', 50)
|
||||
wave_frequency = motion_params.get('wave_frequency', 2)
|
||||
|
||||
# Base linear motion
|
||||
base_x = interpolate(start_pos[0], end_pos[0], t, easing)
|
||||
base_y = interpolate(start_pos[1], end_pos[1], t, easing)
|
||||
|
||||
# Add wave offset perpendicular to motion direction
|
||||
dx = end_pos[0] - start_pos[0]
|
||||
dy = end_pos[1] - start_pos[1]
|
||||
length = math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if length > 0:
|
||||
# Perpendicular direction
|
||||
perp_x = -dy / length
|
||||
perp_y = dx / length
|
||||
|
||||
# Wave offset
|
||||
wave_offset = math.sin(t * wave_frequency * 2 * math.pi) * wave_amplitude
|
||||
|
||||
x = base_x + perp_x * wave_offset
|
||||
y = base_y + perp_y * wave_offset
|
||||
else:
|
||||
x, y = base_x, base_y
|
||||
|
||||
elif motion_type == 'bezier':
|
||||
# Quadratic bezier curve
|
||||
control_point = motion_params.get('control_point', (
|
||||
(start_pos[0] + end_pos[0]) // 2,
|
||||
(start_pos[1] + end_pos[1]) // 2 - 100
|
||||
))
|
||||
|
||||
# Quadratic Bezier formula: B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2
|
||||
x = (1 - t) ** 2 * start_pos[0] + 2 * (1 - t) * t * control_point[0] + t ** 2 * end_pos[0]
|
||||
y = (1 - t) ** 2 * start_pos[1] + 2 * (1 - t) * t * control_point[1] + t ** 2 * end_pos[1]
|
||||
|
||||
else:
|
||||
# Default to linear
|
||||
x = interpolate(start_pos[0], end_pos[0], t, easing)
|
||||
y = interpolate(start_pos[1], end_pos[1], t, easing)
|
||||
|
||||
# Draw object at calculated position
|
||||
x, y = int(x), int(y)
|
||||
|
||||
if object_type == 'circle':
|
||||
draw_circle(
|
||||
frame,
|
||||
center=(x, y),
|
||||
radius=object_data['radius'],
|
||||
fill_color=object_data['color']
|
||||
)
|
||||
elif object_type == 'emoji':
|
||||
draw_emoji_enhanced(
|
||||
frame,
|
||||
emoji=object_data['emoji'],
|
||||
position=(x - object_data['size'] // 2, y - object_data['size'] // 2),
|
||||
size=object_data['size'],
|
||||
shadow=object_data.get('shadow', True)
|
||||
)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
def create_path_from_points(points: list[tuple[int, int]],
|
||||
num_frames: int = 60,
|
||||
easing: str = 'ease_in_out') -> list[tuple[int, int]]:
|
||||
"""
|
||||
Create a smooth path through multiple points.
|
||||
|
||||
Args:
|
||||
points: List of (x, y) waypoints
|
||||
num_frames: Total number of frames
|
||||
easing: Easing between points
|
||||
|
||||
Returns:
|
||||
List of (x, y) positions for each frame
|
||||
"""
|
||||
if len(points) < 2:
|
||||
return points * num_frames
|
||||
|
||||
path = []
|
||||
frames_per_segment = num_frames // (len(points) - 1)
|
||||
|
||||
for i in range(len(points) - 1):
|
||||
start = points[i]
|
||||
end = points[i + 1]
|
||||
|
||||
# Last segment gets remaining frames
|
||||
if i == len(points) - 2:
|
||||
segment_frames = num_frames - len(path)
|
||||
else:
|
||||
segment_frames = frames_per_segment
|
||||
|
||||
for j in range(segment_frames):
|
||||
t = j / segment_frames if segment_frames > 0 else 0
|
||||
x = interpolate(start[0], end[0], t, easing)
|
||||
y = interpolate(start[1], end[1], t, easing)
|
||||
path.append((int(x), int(y)))
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def apply_trail_effect(frames: list, trail_length: int = 5,
|
||||
fade_alpha: float = 0.3) -> list:
|
||||
"""
|
||||
Add motion trail effect to moving object.
|
||||
|
||||
Args:
|
||||
frames: List of frames with moving object
|
||||
trail_length: Number of previous frames to blend
|
||||
fade_alpha: Opacity of trail frames
|
||||
|
||||
Returns:
|
||||
List of frames with trail effect
|
||||
"""
|
||||
from PIL import Image, ImageChops
|
||||
import numpy as np
|
||||
|
||||
trailed_frames = []
|
||||
|
||||
for i, frame in enumerate(frames):
|
||||
# Start with current frame
|
||||
result = frame.copy()
|
||||
|
||||
# Blend previous frames
|
||||
for j in range(1, min(trail_length + 1, i + 1)):
|
||||
prev_frame = frames[i - j]
|
||||
|
||||
# Calculate fade
|
||||
alpha = fade_alpha ** j
|
||||
|
||||
# Blend
|
||||
result_array = np.array(result, dtype=np.float32)
|
||||
prev_array = np.array(prev_frame, dtype=np.float32)
|
||||
|
||||
blended = result_array * (1 - alpha) + prev_array * alpha
|
||||
result = Image.fromarray(blended.astype(np.uint8))
|
||||
|
||||
trailed_frames.append(result)
|
||||
|
||||
return trailed_frames
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == '__main__':
|
||||
print("Creating movement examples...")
|
||||
|
||||
# Example 1: Linear movement
|
||||
builder = GIFBuilder(width=480, height=480, fps=20)
|
||||
frames = create_move_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '🚀', 'size': 60},
|
||||
start_pos=(50, 240),
|
||||
end_pos=(430, 240),
|
||||
num_frames=30,
|
||||
motion_type='linear',
|
||||
easing='ease_out'
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('move_linear.gif', num_colors=128)
|
||||
|
||||
# Example 2: Arc movement
|
||||
builder.clear()
|
||||
frames = create_move_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '⚽', 'size': 60},
|
||||
start_pos=(50, 350),
|
||||
end_pos=(430, 350),
|
||||
num_frames=30,
|
||||
motion_type='arc',
|
||||
motion_params={'arc_height': 150},
|
||||
easing='linear'
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('move_arc.gif', num_colors=128)
|
||||
|
||||
# Example 3: Circular movement
|
||||
builder.clear()
|
||||
frames = create_move_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '🌍', 'size': 50},
|
||||
start_pos=(0, 0), # Ignored for circle
|
||||
end_pos=(0, 0), # Ignored for circle
|
||||
num_frames=40,
|
||||
motion_type='circle',
|
||||
motion_params={
|
||||
'center': (240, 240),
|
||||
'radius': 120,
|
||||
'start_angle': 0,
|
||||
'angle_range': 360
|
||||
},
|
||||
easing='linear'
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('move_circle.gif', num_colors=128)
|
||||
|
||||
print("Created movement examples!")
|
||||
Reference in New Issue
Block a user