Initial commit
This commit is contained in:
269
skills/slack-gif-creator/templates/spin.py
Executable file
269
skills/slack-gif-creator/templates/spin.py
Executable file
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Spin Animation - Rotate objects continuously or with variation.
|
||||
|
||||
Creates spinning, rotating, and wobbling effects.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import math
|
||||
|
||||
sys.path.append(str(Path(__file__).parent.parent))
|
||||
|
||||
from PIL import Image
|
||||
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_spin_animation(
|
||||
object_type: str = 'emoji',
|
||||
object_data: dict | None = None,
|
||||
num_frames: int = 30,
|
||||
rotation_type: str = 'clockwise', # 'clockwise', 'counterclockwise', 'wobble', 'pendulum'
|
||||
full_rotations: float = 1.0,
|
||||
easing: str = 'linear',
|
||||
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 spinning/rotating animation.
|
||||
|
||||
Args:
|
||||
object_type: 'emoji', 'image', 'text'
|
||||
object_data: Object configuration
|
||||
num_frames: Number of frames
|
||||
rotation_type: Type of rotation
|
||||
full_rotations: Number of complete 360° rotations
|
||||
easing: Easing function for rotation speed
|
||||
center_pos: Center position for rotation
|
||||
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}
|
||||
|
||||
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 rotation angle
|
||||
if rotation_type == 'clockwise':
|
||||
angle = interpolate(0, 360 * full_rotations, t, easing)
|
||||
elif rotation_type == 'counterclockwise':
|
||||
angle = interpolate(0, -360 * full_rotations, t, easing)
|
||||
elif rotation_type == 'wobble':
|
||||
# Back and forth rotation
|
||||
angle = math.sin(t * full_rotations * 2 * math.pi) * 45
|
||||
elif rotation_type == 'pendulum':
|
||||
# Smooth pendulum swing
|
||||
angle = math.sin(t * full_rotations * 2 * math.pi) * 90
|
||||
else:
|
||||
angle = interpolate(0, 360 * full_rotations, t, easing)
|
||||
|
||||
# Create object on transparent background to rotate
|
||||
if object_type == 'emoji':
|
||||
# For emoji, we need to create a larger canvas to avoid clipping during rotation
|
||||
emoji_size = object_data['size']
|
||||
canvas_size = int(emoji_size * 1.5)
|
||||
emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
||||
|
||||
# Draw emoji in center of canvas
|
||||
from core.frame_composer import draw_emoji_enhanced
|
||||
draw_emoji_enhanced(
|
||||
emoji_canvas,
|
||||
emoji=object_data['emoji'],
|
||||
position=(canvas_size // 2 - emoji_size // 2, canvas_size // 2 - emoji_size // 2),
|
||||
size=emoji_size,
|
||||
shadow=False
|
||||
)
|
||||
|
||||
# Rotate the canvas
|
||||
rotated = emoji_canvas.rotate(angle, resample=Image.BICUBIC, expand=False)
|
||||
|
||||
# Paste onto frame
|
||||
paste_x = center_pos[0] - canvas_size // 2
|
||||
paste_y = center_pos[1] - canvas_size // 2
|
||||
frame.paste(rotated, (paste_x, paste_y), rotated)
|
||||
|
||||
elif object_type == 'text':
|
||||
from core.typography import draw_text_with_outline
|
||||
# Similar approach - create canvas, draw text, rotate
|
||||
text = object_data.get('text', 'SPIN!')
|
||||
font_size = object_data.get('font_size', 50)
|
||||
|
||||
canvas_size = max(frame_width, frame_height)
|
||||
text_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
||||
|
||||
# Draw text
|
||||
text_canvas_rgb = text_canvas.convert('RGB')
|
||||
text_canvas_rgb.paste(bg_color, (0, 0, canvas_size, canvas_size))
|
||||
draw_text_with_outline(
|
||||
text_canvas_rgb,
|
||||
text,
|
||||
position=(canvas_size // 2, canvas_size // 2),
|
||||
font_size=font_size,
|
||||
text_color=object_data.get('text_color', (0, 0, 0)),
|
||||
outline_color=object_data.get('outline_color', (255, 255, 255)),
|
||||
outline_width=3,
|
||||
centered=True
|
||||
)
|
||||
|
||||
# Convert back to RGBA for rotation
|
||||
text_canvas = text_canvas_rgb.convert('RGBA')
|
||||
|
||||
# Make background transparent
|
||||
data = text_canvas.getdata()
|
||||
new_data = []
|
||||
for item in data:
|
||||
if item[:3] == bg_color:
|
||||
new_data.append((255, 255, 255, 0))
|
||||
else:
|
||||
new_data.append(item)
|
||||
text_canvas.putdata(new_data)
|
||||
|
||||
# Rotate
|
||||
rotated = text_canvas.rotate(angle, resample=Image.BICUBIC, expand=False)
|
||||
|
||||
# Composite onto frame
|
||||
frame_rgba = frame.convert('RGBA')
|
||||
frame_rgba = Image.alpha_composite(frame_rgba, rotated)
|
||||
frame = frame_rgba.convert('RGB')
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
def create_loading_spinner(
|
||||
num_frames: int = 20,
|
||||
spinner_type: str = 'dots', # 'dots', 'arc', 'emoji'
|
||||
size: int = 100,
|
||||
color: tuple[int, int, int] = (100, 150, 255),
|
||||
frame_width: int = 128,
|
||||
frame_height: int = 128,
|
||||
bg_color: tuple[int, int, int] = (255, 255, 255)
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create a loading spinner animation.
|
||||
|
||||
Args:
|
||||
num_frames: Number of frames
|
||||
spinner_type: Type of spinner
|
||||
size: Spinner size
|
||||
color: Spinner color
|
||||
frame_width: Frame width
|
||||
frame_height: Frame height
|
||||
bg_color: Background color
|
||||
|
||||
Returns:
|
||||
List of frames
|
||||
"""
|
||||
from PIL import ImageDraw
|
||||
frames = []
|
||||
center = (frame_width // 2, frame_height // 2)
|
||||
|
||||
for i in range(num_frames):
|
||||
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
||||
draw = ImageDraw.Draw(frame)
|
||||
|
||||
angle_offset = (i / num_frames) * 360
|
||||
|
||||
if spinner_type == 'dots':
|
||||
# Circular dots
|
||||
num_dots = 8
|
||||
for j in range(num_dots):
|
||||
angle = (j / num_dots * 360 + angle_offset) * math.pi / 180
|
||||
x = center[0] + size * 0.4 * math.cos(angle)
|
||||
y = center[1] + size * 0.4 * math.sin(angle)
|
||||
|
||||
# Fade based on position
|
||||
alpha = 1.0 - (j / num_dots)
|
||||
dot_color = tuple(int(c * alpha) for c in color)
|
||||
dot_radius = int(size * 0.1)
|
||||
|
||||
draw.ellipse(
|
||||
[x - dot_radius, y - dot_radius, x + dot_radius, y + dot_radius],
|
||||
fill=dot_color
|
||||
)
|
||||
|
||||
elif spinner_type == 'arc':
|
||||
# Rotating arc
|
||||
start_angle = angle_offset
|
||||
end_angle = angle_offset + 270
|
||||
arc_width = int(size * 0.15)
|
||||
|
||||
bbox = [
|
||||
center[0] - size // 2,
|
||||
center[1] - size // 2,
|
||||
center[0] + size // 2,
|
||||
center[1] + size // 2
|
||||
]
|
||||
draw.arc(bbox, start_angle, end_angle, fill=color, width=arc_width)
|
||||
|
||||
elif spinner_type == 'emoji':
|
||||
# Rotating emoji spinner
|
||||
angle = angle_offset
|
||||
emoji_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
||||
draw_emoji_enhanced(
|
||||
emoji_canvas,
|
||||
emoji='⏳',
|
||||
position=(center[0] - size // 2, center[1] - size // 2),
|
||||
size=size,
|
||||
shadow=False
|
||||
)
|
||||
rotated = emoji_canvas.rotate(angle, center=center, resample=Image.BICUBIC)
|
||||
frame.paste(rotated, (0, 0), rotated)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == '__main__':
|
||||
print("Creating spin animations...")
|
||||
|
||||
builder = GIFBuilder(width=480, height=480, fps=20)
|
||||
|
||||
# Example 1: Clockwise spin
|
||||
frames = create_spin_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '🔄', 'size': 100},
|
||||
num_frames=30,
|
||||
rotation_type='clockwise',
|
||||
full_rotations=2
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('spin_clockwise.gif', num_colors=128)
|
||||
|
||||
# Example 2: Wobble
|
||||
builder.clear()
|
||||
frames = create_spin_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '🎯', 'size': 100},
|
||||
num_frames=30,
|
||||
rotation_type='wobble',
|
||||
full_rotations=3
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('spin_wobble.gif', num_colors=128)
|
||||
|
||||
# Example 3: Loading spinner
|
||||
builder = GIFBuilder(width=128, height=128, fps=15)
|
||||
frames = create_loading_spinner(num_frames=20, spinner_type='dots')
|
||||
builder.add_frames(frames)
|
||||
builder.save('loading_spinner.gif', num_colors=64, optimize_for_emoji=True)
|
||||
|
||||
print("Created spin animations!")
|
||||
Reference in New Issue
Block a user