Initial commit
This commit is contained in:
300
skills/slack-gif-creator/templates/wiggle.py
Executable file
300
skills/slack-gif-creator/templates/wiggle.py
Executable file
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Wiggle Animation - Smooth, organic wobbling and jiggling motions.
|
||||
|
||||
Creates playful, elastic movements that are smoother than shake.
|
||||
"""
|
||||
|
||||
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
|
||||
from core.easing import interpolate
|
||||
|
||||
|
||||
def create_wiggle_animation(
|
||||
object_type: str = 'emoji',
|
||||
object_data: dict | None = None,
|
||||
num_frames: int = 30,
|
||||
wiggle_type: str = 'jello', # 'jello', 'wave', 'bounce', 'sway'
|
||||
intensity: float = 1.0,
|
||||
cycles: float = 2.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 wiggle/wobble animation.
|
||||
|
||||
Args:
|
||||
object_type: 'emoji', 'text'
|
||||
object_data: Object configuration
|
||||
num_frames: Number of frames
|
||||
wiggle_type: Type of wiggle motion
|
||||
intensity: Wiggle intensity multiplier
|
||||
cycles: Number of wiggle cycles
|
||||
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}
|
||||
|
||||
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)
|
||||
|
||||
# Calculate wiggle transformations
|
||||
offset_x = 0
|
||||
offset_y = 0
|
||||
rotation = 0
|
||||
scale_x = 1.0
|
||||
scale_y = 1.0
|
||||
|
||||
if wiggle_type == 'jello':
|
||||
# Jello wobble - multiple frequencies
|
||||
freq1 = cycles * 2 * math.pi
|
||||
freq2 = cycles * 3 * math.pi
|
||||
freq3 = cycles * 5 * math.pi
|
||||
|
||||
decay = 1.0 - t if cycles < 1.5 else 1.0 # Decay for single wiggles
|
||||
|
||||
offset_x = (
|
||||
math.sin(freq1 * t) * 15 +
|
||||
math.sin(freq2 * t) * 8 +
|
||||
math.sin(freq3 * t) * 3
|
||||
) * intensity * decay
|
||||
|
||||
rotation = (
|
||||
math.sin(freq1 * t) * 10 +
|
||||
math.cos(freq2 * t) * 5
|
||||
) * intensity * decay
|
||||
|
||||
# Squash and stretch
|
||||
scale_y = 1.0 + math.sin(freq1 * t) * 0.1 * intensity * decay
|
||||
scale_x = 1.0 / scale_y # Preserve volume
|
||||
|
||||
elif wiggle_type == 'wave':
|
||||
# Wave motion
|
||||
freq = cycles * 2 * math.pi
|
||||
offset_y = math.sin(freq * t) * 20 * intensity
|
||||
rotation = math.sin(freq * t + math.pi / 4) * 8 * intensity
|
||||
|
||||
elif wiggle_type == 'bounce':
|
||||
# Bouncy wiggle
|
||||
freq = cycles * 2 * math.pi
|
||||
bounce = abs(math.sin(freq * t))
|
||||
|
||||
scale_y = 1.0 + bounce * 0.2 * intensity
|
||||
scale_x = 1.0 - bounce * 0.1 * intensity
|
||||
offset_y = -bounce * 10 * intensity
|
||||
|
||||
elif wiggle_type == 'sway':
|
||||
# Gentle sway back and forth
|
||||
freq = cycles * 2 * math.pi
|
||||
offset_x = math.sin(freq * t) * 25 * intensity
|
||||
rotation = math.sin(freq * t) * 12 * intensity
|
||||
|
||||
# Subtle scale change
|
||||
scale = 1.0 + math.sin(freq * t) * 0.05 * intensity
|
||||
scale_x = scale
|
||||
scale_y = scale
|
||||
|
||||
elif wiggle_type == 'tail_wag':
|
||||
# Like a wagging tail - base stays, tip moves
|
||||
freq = cycles * 2 * math.pi
|
||||
wag = math.sin(freq * t) * intensity
|
||||
|
||||
# Rotation focused at one end
|
||||
rotation = wag * 20
|
||||
offset_x = wag * 15
|
||||
|
||||
# Apply transformations
|
||||
if object_type == 'emoji':
|
||||
size = object_data['size']
|
||||
size_x = int(size * scale_x)
|
||||
size_y = int(size * scale_y)
|
||||
|
||||
# For non-uniform scaling or rotation, we need to use PIL transforms
|
||||
if abs(scale_x - scale_y) > 0.01 or abs(rotation) > 0.1:
|
||||
# Create emoji on transparent canvas
|
||||
canvas_size = int(size * 2)
|
||||
emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
||||
|
||||
# Draw emoji
|
||||
draw_emoji_enhanced(
|
||||
emoji_canvas,
|
||||
emoji=object_data['emoji'],
|
||||
position=(canvas_size // 2 - size // 2, canvas_size // 2 - size // 2),
|
||||
size=size,
|
||||
shadow=False
|
||||
)
|
||||
|
||||
# Scale
|
||||
if abs(scale_x - scale_y) > 0.01:
|
||||
new_size = (int(canvas_size * scale_x), int(canvas_size * scale_y))
|
||||
emoji_canvas = emoji_canvas.resize(new_size, Image.LANCZOS)
|
||||
canvas_size_x, canvas_size_y = new_size
|
||||
else:
|
||||
canvas_size_x = canvas_size_y = canvas_size
|
||||
|
||||
# Rotate
|
||||
if abs(rotation) > 0.1:
|
||||
emoji_canvas = emoji_canvas.rotate(
|
||||
rotation,
|
||||
resample=Image.BICUBIC,
|
||||
expand=False
|
||||
)
|
||||
|
||||
# Position with offset
|
||||
paste_x = int(center_pos[0] - canvas_size_x // 2 + offset_x)
|
||||
paste_y = int(center_pos[1] - canvas_size_y // 2 + offset_y)
|
||||
|
||||
frame_rgba = frame.convert('RGBA')
|
||||
frame_rgba.paste(emoji_canvas, (paste_x, paste_y), emoji_canvas)
|
||||
frame = frame_rgba.convert('RGB')
|
||||
else:
|
||||
# Simple case - just offset
|
||||
pos_x = int(center_pos[0] - size // 2 + offset_x)
|
||||
pos_y = int(center_pos[1] - size // 2 + offset_y)
|
||||
draw_emoji_enhanced(
|
||||
frame,
|
||||
emoji=object_data['emoji'],
|
||||
position=(pos_x, pos_y),
|
||||
size=size,
|
||||
shadow=object_data.get('shadow', True)
|
||||
)
|
||||
|
||||
elif object_type == 'text':
|
||||
from core.typography import draw_text_with_outline
|
||||
|
||||
# Create text on canvas for transformation
|
||||
canvas_size = max(frame_width, frame_height)
|
||||
text_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
||||
|
||||
# Convert to RGB for drawing
|
||||
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=object_data.get('text', 'WIGGLE'),
|
||||
position=(canvas_size // 2, canvas_size // 2),
|
||||
font_size=object_data.get('font_size', 50),
|
||||
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
|
||||
)
|
||||
|
||||
# Make transparent
|
||||
text_canvas = text_canvas_rgb.convert('RGBA')
|
||||
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)
|
||||
|
||||
# Apply rotation
|
||||
if abs(rotation) > 0.1:
|
||||
text_canvas = text_canvas.rotate(rotation, center=(canvas_size // 2, canvas_size // 2), resample=Image.BICUBIC)
|
||||
|
||||
# Crop to frame with offset
|
||||
left = (canvas_size - frame_width) // 2 - int(offset_x)
|
||||
top = (canvas_size - frame_height) // 2 - int(offset_y)
|
||||
text_cropped = text_canvas.crop((left, top, left + frame_width, top + frame_height))
|
||||
|
||||
frame_rgba = frame.convert('RGBA')
|
||||
frame = Image.alpha_composite(frame_rgba, text_cropped)
|
||||
frame = frame.convert('RGB')
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
def create_excited_wiggle(
|
||||
emoji: str = '🎉',
|
||||
num_frames: int = 20,
|
||||
frame_size: int = 128
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create excited wiggle for emoji GIFs.
|
||||
|
||||
Args:
|
||||
emoji: Emoji to wiggle
|
||||
num_frames: Number of frames
|
||||
frame_size: Frame size (square)
|
||||
|
||||
Returns:
|
||||
List of frames
|
||||
"""
|
||||
return create_wiggle_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': emoji, 'size': 80, 'shadow': False},
|
||||
num_frames=num_frames,
|
||||
wiggle_type='jello',
|
||||
intensity=0.8,
|
||||
cycles=2,
|
||||
center_pos=(frame_size // 2, frame_size // 2),
|
||||
frame_width=frame_size,
|
||||
frame_height=frame_size,
|
||||
bg_color=(255, 255, 255)
|
||||
)
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == '__main__':
|
||||
print("Creating wiggle animations...")
|
||||
|
||||
builder = GIFBuilder(width=480, height=480, fps=20)
|
||||
|
||||
# Example 1: Jello wiggle
|
||||
frames = create_wiggle_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '🎈', 'size': 100},
|
||||
num_frames=40,
|
||||
wiggle_type='jello',
|
||||
intensity=1.0,
|
||||
cycles=2
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('wiggle_jello.gif', num_colors=128)
|
||||
|
||||
# Example 2: Wave
|
||||
builder.clear()
|
||||
frames = create_wiggle_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '🌊', 'size': 100},
|
||||
num_frames=30,
|
||||
wiggle_type='wave',
|
||||
intensity=1.2,
|
||||
cycles=3
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('wiggle_wave.gif', num_colors=128)
|
||||
|
||||
# Example 3: Excited wiggle (emoji size)
|
||||
builder = GIFBuilder(width=128, height=128, fps=15)
|
||||
frames = create_excited_wiggle(emoji='🎉', num_frames=20)
|
||||
builder.add_frames(frames)
|
||||
builder.save('wiggle_excited.gif', num_colors=48, optimize_for_emoji=True)
|
||||
|
||||
print("Created wiggle animations!")
|
||||
Reference in New Issue
Block a user