Initial commit
This commit is contained in:
106
skills/slack-gif-creator/templates/bounce.py
Normal file
106
skills/slack-gif-creator/templates/bounce.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bounce Animation Template - Creates bouncing motion for objects.
|
||||
|
||||
Use this to make objects bounce up and down or horizontally with realistic physics.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
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
|
||||
from core.easing import ease_out_bounce, interpolate
|
||||
|
||||
|
||||
def create_bounce_animation(
|
||||
object_type: str = 'circle',
|
||||
object_data: dict = None,
|
||||
num_frames: int = 30,
|
||||
bounce_height: int = 150,
|
||||
ground_y: int = 350,
|
||||
start_x: int = 240,
|
||||
frame_width: int = 480,
|
||||
frame_height: int = 480,
|
||||
bg_color: tuple[int, int, int] = (255, 255, 255)
|
||||
) -> list:
|
||||
"""
|
||||
Create frames for a bouncing animation.
|
||||
|
||||
Args:
|
||||
object_type: 'circle', 'emoji', or 'custom'
|
||||
object_data: Data for the object (e.g., {'radius': 30, 'color': (255, 0, 0)})
|
||||
num_frames: Number of frames in the animation
|
||||
bounce_height: Maximum height of bounce
|
||||
ground_y: Y position of ground
|
||||
start_x: X position (or starting X if moving horizontally)
|
||||
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': (255, 100, 100)}
|
||||
elif object_type == 'emoji':
|
||||
object_data = {'emoji': '⚽', 'size': 60}
|
||||
|
||||
for i in range(num_frames):
|
||||
# Create blank frame
|
||||
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
||||
|
||||
# Calculate progress (0.0 to 1.0)
|
||||
t = i / (num_frames - 1) if num_frames > 1 else 0
|
||||
|
||||
# Calculate Y position using bounce easing
|
||||
y = ground_y - int(ease_out_bounce(t) * bounce_height)
|
||||
|
||||
# Draw object
|
||||
if object_type == 'circle':
|
||||
draw_circle(
|
||||
frame,
|
||||
center=(start_x, y),
|
||||
radius=object_data['radius'],
|
||||
fill_color=object_data['color']
|
||||
)
|
||||
elif object_type == 'emoji':
|
||||
draw_emoji(
|
||||
frame,
|
||||
emoji=object_data['emoji'],
|
||||
position=(start_x - object_data['size'] // 2, y - object_data['size'] // 2),
|
||||
size=object_data['size']
|
||||
)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == '__main__':
|
||||
print("Creating bouncing ball GIF...")
|
||||
|
||||
# Create GIF builder
|
||||
builder = GIFBuilder(width=480, height=480, fps=20)
|
||||
|
||||
# Generate bounce animation
|
||||
frames = create_bounce_animation(
|
||||
object_type='circle',
|
||||
object_data={'radius': 40, 'color': (255, 100, 100)},
|
||||
num_frames=40,
|
||||
bounce_height=200
|
||||
)
|
||||
|
||||
# Add frames to builder
|
||||
builder.add_frames(frames)
|
||||
|
||||
# Save GIF
|
||||
builder.save('bounce_test.gif', num_colors=64)
|
||||
331
skills/slack-gif-creator/templates/explode.py
Normal file
331
skills/slack-gif-creator/templates/explode.py
Normal file
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Explode Animation - Break objects into pieces that fly outward.
|
||||
|
||||
Creates explosion, shatter, and particle burst effects.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import math
|
||||
import random
|
||||
|
||||
sys.path.append(str(Path(__file__).parent.parent))
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
import numpy as np
|
||||
from core.gif_builder import GIFBuilder
|
||||
from core.frame_composer import create_blank_frame, draw_emoji_enhanced
|
||||
from core.visual_effects import ParticleSystem
|
||||
from core.easing import interpolate
|
||||
|
||||
|
||||
def create_explode_animation(
|
||||
object_type: str = 'emoji',
|
||||
object_data: dict | None = None,
|
||||
num_frames: int = 30,
|
||||
explode_type: str = 'burst', # 'burst', 'shatter', 'dissolve', 'implode'
|
||||
num_pieces: int = 20,
|
||||
explosion_speed: float = 5.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 explosion animation.
|
||||
|
||||
Args:
|
||||
object_type: 'emoji', 'circle', 'text'
|
||||
object_data: Object configuration
|
||||
num_frames: Number of frames
|
||||
explode_type: Type of explosion
|
||||
num_pieces: Number of pieces/particles
|
||||
explosion_speed: Speed of explosion
|
||||
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}
|
||||
|
||||
# Generate pieces/particles
|
||||
pieces = []
|
||||
for _ in range(num_pieces):
|
||||
angle = random.uniform(0, 2 * math.pi)
|
||||
speed = random.uniform(explosion_speed * 0.5, explosion_speed * 1.5)
|
||||
vx = math.cos(angle) * speed
|
||||
vy = math.sin(angle) * speed
|
||||
size = random.randint(3, 12)
|
||||
color = (
|
||||
random.randint(100, 255),
|
||||
random.randint(100, 255),
|
||||
random.randint(100, 255)
|
||||
)
|
||||
rotation_speed = random.uniform(-20, 20)
|
||||
|
||||
pieces.append({
|
||||
'vx': vx,
|
||||
'vy': vy,
|
||||
'size': size,
|
||||
'color': color,
|
||||
'rotation': 0,
|
||||
'rotation_speed': rotation_speed
|
||||
})
|
||||
|
||||
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)
|
||||
draw = ImageDraw.Draw(frame)
|
||||
|
||||
if explode_type == 'burst':
|
||||
# Show object at start, then explode
|
||||
if t < 0.2:
|
||||
# Object still intact
|
||||
scale = interpolate(1.0, 1.2, t / 0.2, 'ease_out')
|
||||
if object_type == 'emoji':
|
||||
size = int(object_data['size'] * scale)
|
||||
draw_emoji_enhanced(
|
||||
frame,
|
||||
emoji=object_data['emoji'],
|
||||
position=(center_pos[0] - size // 2, center_pos[1] - size // 2),
|
||||
size=size,
|
||||
shadow=False
|
||||
)
|
||||
else:
|
||||
# Exploded - draw pieces
|
||||
explosion_t = (t - 0.2) / 0.8
|
||||
for piece in pieces:
|
||||
# Update position
|
||||
x = center_pos[0] + piece['vx'] * explosion_t * 50
|
||||
y = center_pos[1] + piece['vy'] * explosion_t * 50 + 0.5 * 300 * explosion_t ** 2 # Gravity
|
||||
|
||||
# Fade out
|
||||
alpha = 1.0 - explosion_t
|
||||
if alpha > 0:
|
||||
color = tuple(int(c * alpha) for c in piece['color'])
|
||||
size = int(piece['size'] * (1 - explosion_t * 0.5))
|
||||
|
||||
draw.ellipse(
|
||||
[x - size, y - size, x + size, y + size],
|
||||
fill=color
|
||||
)
|
||||
|
||||
elif explode_type == 'shatter':
|
||||
# Break into geometric pieces
|
||||
if t < 0.15:
|
||||
# Object intact
|
||||
if object_type == 'emoji':
|
||||
draw_emoji_enhanced(
|
||||
frame,
|
||||
emoji=object_data['emoji'],
|
||||
position=(center_pos[0] - object_data['size'] // 2,
|
||||
center_pos[1] - object_data['size'] // 2),
|
||||
size=object_data['size'],
|
||||
shadow=False
|
||||
)
|
||||
else:
|
||||
# Shattered
|
||||
shatter_t = (t - 0.15) / 0.85
|
||||
|
||||
# Draw triangular shards
|
||||
for piece in pieces[:min(10, len(pieces))]:
|
||||
x = center_pos[0] + piece['vx'] * shatter_t * 30
|
||||
y = center_pos[1] + piece['vy'] * shatter_t * 30 + 0.5 * 200 * shatter_t ** 2
|
||||
|
||||
# Update rotation
|
||||
rotation = piece['rotation_speed'] * shatter_t * 100
|
||||
|
||||
# Draw triangle shard
|
||||
shard_size = piece['size'] * 2
|
||||
points = []
|
||||
for j in range(3):
|
||||
angle = (rotation + j * 120) * math.pi / 180
|
||||
px = x + shard_size * math.cos(angle)
|
||||
py = y + shard_size * math.sin(angle)
|
||||
points.append((px, py))
|
||||
|
||||
alpha = 1.0 - shatter_t
|
||||
if alpha > 0:
|
||||
color = tuple(int(c * alpha) for c in piece['color'])
|
||||
draw.polygon(points, fill=color)
|
||||
|
||||
elif explode_type == 'dissolve':
|
||||
# Dissolve into particles
|
||||
dissolve_scale = interpolate(1.0, 0.0, t, 'ease_in')
|
||||
|
||||
if dissolve_scale > 0.1:
|
||||
# Draw fading object
|
||||
if object_type == 'emoji':
|
||||
size = int(object_data['size'] * dissolve_scale)
|
||||
size = max(12, size)
|
||||
|
||||
emoji_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
||||
draw_emoji_enhanced(
|
||||
emoji_canvas,
|
||||
emoji=object_data['emoji'],
|
||||
position=(center_pos[0] - size // 2, center_pos[1] - size // 2),
|
||||
size=size,
|
||||
shadow=False
|
||||
)
|
||||
|
||||
# Apply opacity
|
||||
from templates.fade import apply_opacity
|
||||
emoji_canvas = apply_opacity(emoji_canvas, dissolve_scale)
|
||||
|
||||
frame_rgba = frame.convert('RGBA')
|
||||
frame = Image.alpha_composite(frame_rgba, emoji_canvas)
|
||||
frame = frame.convert('RGB')
|
||||
draw = ImageDraw.Draw(frame)
|
||||
|
||||
# Draw outward-moving particles
|
||||
for piece in pieces:
|
||||
x = center_pos[0] + piece['vx'] * t * 40
|
||||
y = center_pos[1] + piece['vy'] * t * 40
|
||||
|
||||
alpha = 1.0 - t
|
||||
if alpha > 0:
|
||||
color = tuple(int(c * alpha) for c in piece['color'])
|
||||
size = int(piece['size'] * (1 - t * 0.5))
|
||||
draw.ellipse(
|
||||
[x - size, y - size, x + size, y + size],
|
||||
fill=color
|
||||
)
|
||||
|
||||
elif explode_type == 'implode':
|
||||
# Reverse explosion - pieces fly inward
|
||||
if t < 0.7:
|
||||
# Pieces converging
|
||||
implode_t = 1.0 - (t / 0.7)
|
||||
for piece in pieces:
|
||||
x = center_pos[0] + piece['vx'] * implode_t * 50
|
||||
y = center_pos[1] + piece['vy'] * implode_t * 50
|
||||
|
||||
alpha = 1.0 - (1.0 - implode_t) * 0.5
|
||||
color = tuple(int(c * alpha) for c in piece['color'])
|
||||
size = int(piece['size'] * alpha)
|
||||
|
||||
draw.ellipse(
|
||||
[x - size, y - size, x + size, y + size],
|
||||
fill=color
|
||||
)
|
||||
else:
|
||||
# Object reforms
|
||||
reform_t = (t - 0.7) / 0.3
|
||||
scale = interpolate(0.5, 1.0, reform_t, 'elastic_out')
|
||||
|
||||
if object_type == 'emoji':
|
||||
size = int(object_data['size'] * scale)
|
||||
draw_emoji_enhanced(
|
||||
frame,
|
||||
emoji=object_data['emoji'],
|
||||
position=(center_pos[0] - size // 2, center_pos[1] - size // 2),
|
||||
size=size,
|
||||
shadow=False
|
||||
)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
def create_particle_burst(
|
||||
num_frames: int = 25,
|
||||
particle_count: int = 30,
|
||||
center_pos: tuple[int, int] = (240, 240),
|
||||
colors: list[tuple[int, int, int]] | None = None,
|
||||
frame_width: int = 480,
|
||||
frame_height: int = 480,
|
||||
bg_color: tuple[int, int, int] = (255, 255, 255)
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create simple particle burst effect.
|
||||
|
||||
Args:
|
||||
num_frames: Number of frames
|
||||
particle_count: Number of particles
|
||||
center_pos: Burst center
|
||||
colors: Particle colors (None for random)
|
||||
frame_width: Frame width
|
||||
frame_height: Frame height
|
||||
bg_color: Background color
|
||||
|
||||
Returns:
|
||||
List of frames
|
||||
"""
|
||||
particles = ParticleSystem()
|
||||
|
||||
# Emit particles
|
||||
if colors is None:
|
||||
from core.color_palettes import get_palette
|
||||
palette = get_palette('vibrant')
|
||||
colors = [palette['primary'], palette['secondary'], palette['accent']]
|
||||
|
||||
for _ in range(particle_count):
|
||||
color = random.choice(colors)
|
||||
particles.emit(
|
||||
center_pos[0], center_pos[1],
|
||||
count=1,
|
||||
speed=random.uniform(3, 8),
|
||||
color=color,
|
||||
lifetime=random.uniform(20, 30),
|
||||
size=random.randint(3, 8),
|
||||
shape='star'
|
||||
)
|
||||
|
||||
frames = []
|
||||
for _ in range(num_frames):
|
||||
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
||||
|
||||
particles.update()
|
||||
particles.render(frame)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == '__main__':
|
||||
print("Creating explode animations...")
|
||||
|
||||
builder = GIFBuilder(width=480, height=480, fps=20)
|
||||
|
||||
# Example 1: Burst
|
||||
frames = create_explode_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '💣', 'size': 100},
|
||||
num_frames=30,
|
||||
explode_type='burst',
|
||||
num_pieces=25
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('explode_burst.gif', num_colors=128)
|
||||
|
||||
# Example 2: Shatter
|
||||
builder.clear()
|
||||
frames = create_explode_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '🪟', 'size': 100},
|
||||
num_frames=30,
|
||||
explode_type='shatter',
|
||||
num_pieces=12
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('explode_shatter.gif', num_colors=128)
|
||||
|
||||
# Example 3: Particle burst
|
||||
builder.clear()
|
||||
frames = create_particle_burst(num_frames=25, particle_count=40)
|
||||
builder.add_frames(frames)
|
||||
builder.save('explode_particles.gif', num_colors=128)
|
||||
|
||||
print("Created explode animations!")
|
||||
329
skills/slack-gif-creator/templates/fade.py
Normal file
329
skills/slack-gif-creator/templates/fade.py
Normal file
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fade Animation - Fade in, fade out, and crossfade effects.
|
||||
|
||||
Creates smooth opacity transitions for appearing, disappearing, and transitioning.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.append(str(Path(__file__).parent.parent))
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
import numpy as np
|
||||
from core.gif_builder import GIFBuilder
|
||||
from core.frame_composer import create_blank_frame, draw_emoji_enhanced
|
||||
from core.easing import interpolate
|
||||
|
||||
|
||||
def create_fade_animation(
|
||||
object_type: str = 'emoji',
|
||||
object_data: dict | None = None,
|
||||
num_frames: int = 30,
|
||||
fade_type: str = 'in', # 'in', 'out', 'in_out', 'blink'
|
||||
easing: str = 'ease_in_out',
|
||||
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 fade animation.
|
||||
|
||||
Args:
|
||||
object_type: 'emoji', 'text', 'image'
|
||||
object_data: Object configuration
|
||||
num_frames: Number of frames
|
||||
fade_type: Type of fade effect
|
||||
easing: Easing function
|
||||
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
|
||||
|
||||
# Calculate opacity based on fade type
|
||||
if fade_type == 'in':
|
||||
opacity = interpolate(0, 1, t, easing)
|
||||
elif fade_type == 'out':
|
||||
opacity = interpolate(1, 0, t, easing)
|
||||
elif fade_type == 'in_out':
|
||||
if t < 0.5:
|
||||
opacity = interpolate(0, 1, t * 2, easing)
|
||||
else:
|
||||
opacity = interpolate(1, 0, (t - 0.5) * 2, easing)
|
||||
elif fade_type == 'blink':
|
||||
# Quick fade out and back in
|
||||
if t < 0.2:
|
||||
opacity = interpolate(1, 0, t / 0.2, 'ease_in')
|
||||
elif t < 0.4:
|
||||
opacity = interpolate(0, 1, (t - 0.2) / 0.2, 'ease_out')
|
||||
else:
|
||||
opacity = 1.0
|
||||
else:
|
||||
opacity = interpolate(0, 1, t, easing)
|
||||
|
||||
# Create background
|
||||
frame_bg = create_blank_frame(frame_width, frame_height, bg_color)
|
||||
|
||||
# Create object layer with transparency
|
||||
if object_type == 'emoji':
|
||||
# Create RGBA canvas for emoji
|
||||
emoji_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
||||
emoji_size = object_data['size']
|
||||
draw_emoji_enhanced(
|
||||
emoji_canvas,
|
||||
emoji=object_data['emoji'],
|
||||
position=(center_pos[0] - emoji_size // 2, center_pos[1] - emoji_size // 2),
|
||||
size=emoji_size,
|
||||
shadow=object_data.get('shadow', False)
|
||||
)
|
||||
|
||||
# Apply opacity
|
||||
emoji_canvas = apply_opacity(emoji_canvas, opacity)
|
||||
|
||||
# Composite onto background
|
||||
frame_bg_rgba = frame_bg.convert('RGBA')
|
||||
frame = Image.alpha_composite(frame_bg_rgba, emoji_canvas)
|
||||
frame = frame.convert('RGB')
|
||||
|
||||
elif object_type == 'text':
|
||||
from core.typography import draw_text_with_outline
|
||||
|
||||
# Create text on separate layer
|
||||
text_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
||||
text_canvas_rgb = text_canvas.convert('RGB')
|
||||
text_canvas_rgb.paste(bg_color, (0, 0, frame_width, frame_height))
|
||||
|
||||
draw_text_with_outline(
|
||||
text_canvas_rgb,
|
||||
text=object_data.get('text', 'FADE'),
|
||||
position=center_pos,
|
||||
font_size=object_data.get('font_size', 60),
|
||||
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 to RGBA and make background 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 opacity
|
||||
text_canvas = apply_opacity(text_canvas, opacity)
|
||||
|
||||
# Composite
|
||||
frame_bg_rgba = frame_bg.convert('RGBA')
|
||||
frame = Image.alpha_composite(frame_bg_rgba, text_canvas)
|
||||
frame = frame.convert('RGB')
|
||||
|
||||
else:
|
||||
frame = frame_bg
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
def apply_opacity(image: Image.Image, opacity: float) -> Image.Image:
|
||||
"""
|
||||
Apply opacity to an RGBA image.
|
||||
|
||||
Args:
|
||||
image: RGBA image
|
||||
opacity: Opacity value (0.0 to 1.0)
|
||||
|
||||
Returns:
|
||||
Image with adjusted opacity
|
||||
"""
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
# Get alpha channel
|
||||
r, g, b, a = image.split()
|
||||
|
||||
# Multiply alpha by opacity
|
||||
a_array = np.array(a, dtype=np.float32)
|
||||
a_array = a_array * opacity
|
||||
a = Image.fromarray(a_array.astype(np.uint8))
|
||||
|
||||
# Merge back
|
||||
return Image.merge('RGBA', (r, g, b, a))
|
||||
|
||||
|
||||
def create_crossfade(
|
||||
object1_data: dict,
|
||||
object2_data: dict,
|
||||
num_frames: int = 30,
|
||||
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]:
|
||||
"""
|
||||
Crossfade between two objects.
|
||||
|
||||
Args:
|
||||
object1_data: First object configuration
|
||||
object2_data: Second object configuration
|
||||
num_frames: Number of frames
|
||||
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
|
||||
|
||||
# Calculate opacities
|
||||
opacity1 = interpolate(1, 0, t, easing)
|
||||
opacity2 = interpolate(0, 1, t, easing)
|
||||
|
||||
# Create background
|
||||
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
||||
|
||||
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
|
||||
)
|
||||
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')
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
def create_fade_to_color(
|
||||
start_color: tuple[int, int, int],
|
||||
end_color: tuple[int, int, int],
|
||||
num_frames: int = 20,
|
||||
easing: str = 'linear',
|
||||
frame_width: int = 480,
|
||||
frame_height: int = 480
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Fade from one solid color to another.
|
||||
|
||||
Args:
|
||||
start_color: Starting RGB color
|
||||
end_color: Ending RGB color
|
||||
num_frames: Number of frames
|
||||
easing: Easing function
|
||||
frame_width: Frame width
|
||||
frame_height: Frame height
|
||||
|
||||
Returns:
|
||||
List of frames
|
||||
"""
|
||||
frames = []
|
||||
|
||||
for i in range(num_frames):
|
||||
t = i / (num_frames - 1) if num_frames > 1 else 0
|
||||
|
||||
# Interpolate each color channel
|
||||
r = int(interpolate(start_color[0], end_color[0], t, easing))
|
||||
g = int(interpolate(start_color[1], end_color[1], t, easing))
|
||||
b = int(interpolate(start_color[2], end_color[2], t, easing))
|
||||
|
||||
color = (r, g, b)
|
||||
frame = create_blank_frame(frame_width, frame_height, color)
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == '__main__':
|
||||
print("Creating fade animations...")
|
||||
|
||||
builder = GIFBuilder(width=480, height=480, fps=20)
|
||||
|
||||
# Example 1: Fade in
|
||||
frames = create_fade_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '✨', 'size': 120},
|
||||
num_frames=30,
|
||||
fade_type='in',
|
||||
easing='ease_out'
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('fade_in.gif', num_colors=128)
|
||||
|
||||
# Example 2: Crossfade
|
||||
builder.clear()
|
||||
frames = create_crossfade(
|
||||
object1_data={'emoji': '😊', 'size': 100},
|
||||
object2_data={'emoji': '😂', 'size': 100},
|
||||
num_frames=30,
|
||||
object_type='emoji'
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('fade_crossfade.gif', num_colors=128)
|
||||
|
||||
# Example 3: Blink
|
||||
builder.clear()
|
||||
frames = create_fade_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '👀', 'size': 100},
|
||||
num_frames=20,
|
||||
fade_type='blink'
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('fade_blink.gif', num_colors=128)
|
||||
|
||||
print("Created fade animations!")
|
||||
291
skills/slack-gif-creator/templates/flip.py
Normal file
291
skills/slack-gif-creator/templates/flip.py
Normal file
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Flip Animation - 3D-style card flip and rotation effects.
|
||||
|
||||
Creates horizontal and vertical flips with perspective.
|
||||
"""
|
||||
|
||||
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_flip_animation(
|
||||
object1_data: dict,
|
||||
object2_data: dict | None = None,
|
||||
num_frames: int = 30,
|
||||
flip_axis: str = 'horizontal', # 'horizontal', 'vertical'
|
||||
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 3D-style flip animation.
|
||||
|
||||
Args:
|
||||
object1_data: First object (front side)
|
||||
object2_data: Second object (back side, None = same as front)
|
||||
num_frames: Number of frames
|
||||
flip_axis: Axis to flip around
|
||||
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 = []
|
||||
|
||||
if object2_data is None:
|
||||
object2_data = object1_data
|
||||
|
||||
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 rotation angle (0 to 180 degrees)
|
||||
angle = interpolate(0, 180, t, easing)
|
||||
|
||||
# Determine which side is visible and calculate scale
|
||||
if angle < 90:
|
||||
# Front side visible
|
||||
current_object = object1_data
|
||||
scale_factor = math.cos(math.radians(angle))
|
||||
else:
|
||||
# Back side visible
|
||||
current_object = object2_data
|
||||
scale_factor = abs(math.cos(math.radians(angle)))
|
||||
|
||||
# Don't draw when edge-on (very thin)
|
||||
if scale_factor < 0.05:
|
||||
frames.append(frame)
|
||||
continue
|
||||
|
||||
if object_type == 'emoji':
|
||||
size = current_object['size']
|
||||
|
||||
# Create emoji on canvas
|
||||
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
|
||||
)
|
||||
|
||||
# Apply flip scaling
|
||||
if flip_axis == 'horizontal':
|
||||
# Scale horizontally for horizontal flip
|
||||
new_width = max(1, int(canvas_size * scale_factor))
|
||||
new_height = canvas_size
|
||||
else:
|
||||
# Scale vertically for vertical flip
|
||||
new_width = canvas_size
|
||||
new_height = max(1, int(canvas_size * scale_factor))
|
||||
|
||||
# Resize to simulate 3D rotation
|
||||
emoji_scaled = emoji_canvas.resize((new_width, new_height), Image.LANCZOS)
|
||||
|
||||
# Position centered
|
||||
paste_x = center_pos[0] - new_width // 2
|
||||
paste_y = center_pos[1] - new_height // 2
|
||||
|
||||
# Composite onto frame
|
||||
frame_rgba = frame.convert('RGBA')
|
||||
frame_rgba.paste(emoji_scaled, (paste_x, paste_y), emoji_scaled)
|
||||
frame = frame_rgba.convert('RGB')
|
||||
|
||||
elif object_type == 'text':
|
||||
from core.typography import draw_text_with_outline
|
||||
|
||||
# Create text on canvas
|
||||
text = current_object.get('text', 'FLIP')
|
||||
font_size = current_object.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 on RGB for text rendering
|
||||
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=text,
|
||||
position=(canvas_size // 2, canvas_size // 2),
|
||||
font_size=font_size,
|
||||
text_color=current_object.get('text_color', (0, 0, 0)),
|
||||
outline_color=current_object.get('outline_color', (255, 255, 255)),
|
||||
outline_width=3,
|
||||
centered=True
|
||||
)
|
||||
|
||||
# Make background 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 flip scaling
|
||||
if flip_axis == 'horizontal':
|
||||
new_width = max(1, int(canvas_size * scale_factor))
|
||||
new_height = canvas_size
|
||||
else:
|
||||
new_width = canvas_size
|
||||
new_height = max(1, int(canvas_size * scale_factor))
|
||||
|
||||
text_scaled = text_canvas.resize((new_width, new_height), Image.LANCZOS)
|
||||
|
||||
# Center and crop
|
||||
if flip_axis == 'horizontal':
|
||||
left = (new_width - frame_width) // 2 if new_width > frame_width else 0
|
||||
top = (canvas_size - frame_height) // 2
|
||||
paste_x = center_pos[0] - min(new_width, frame_width) // 2
|
||||
paste_y = 0
|
||||
|
||||
text_cropped = text_scaled.crop((
|
||||
left,
|
||||
top,
|
||||
left + min(new_width, frame_width),
|
||||
top + frame_height
|
||||
))
|
||||
else:
|
||||
left = (canvas_size - frame_width) // 2
|
||||
top = (new_height - frame_height) // 2 if new_height > frame_height else 0
|
||||
paste_x = 0
|
||||
paste_y = center_pos[1] - min(new_height, frame_height) // 2
|
||||
|
||||
text_cropped = text_scaled.crop((
|
||||
left,
|
||||
top,
|
||||
left + frame_width,
|
||||
top + min(new_height, frame_height)
|
||||
))
|
||||
|
||||
frame_rgba = frame.convert('RGBA')
|
||||
frame_rgba.paste(text_cropped, (paste_x, paste_y), text_cropped)
|
||||
frame = frame_rgba.convert('RGB')
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
def create_quick_flip(
|
||||
emoji_front: str,
|
||||
emoji_back: str,
|
||||
num_frames: int = 20,
|
||||
frame_size: int = 128
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create quick flip for emoji GIFs.
|
||||
|
||||
Args:
|
||||
emoji_front: Front emoji
|
||||
emoji_back: Back emoji
|
||||
num_frames: Number of frames
|
||||
frame_size: Frame size (square)
|
||||
|
||||
Returns:
|
||||
List of frames
|
||||
"""
|
||||
return create_flip_animation(
|
||||
object1_data={'emoji': emoji_front, 'size': 80},
|
||||
object2_data={'emoji': emoji_back, 'size': 80},
|
||||
num_frames=num_frames,
|
||||
flip_axis='horizontal',
|
||||
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_nope_flip(
|
||||
num_frames: int = 25,
|
||||
frame_width: int = 480,
|
||||
frame_height: int = 480
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create "nope" reaction flip (like flipping table).
|
||||
|
||||
Args:
|
||||
num_frames: Number of frames
|
||||
frame_width: Frame width
|
||||
frame_height: Frame height
|
||||
|
||||
Returns:
|
||||
List of frames
|
||||
"""
|
||||
return create_flip_animation(
|
||||
object1_data={'text': 'NOPE', 'font_size': 80, 'text_color': (255, 50, 50)},
|
||||
object2_data={'text': 'NOPE', 'font_size': 80, 'text_color': (255, 50, 50)},
|
||||
num_frames=num_frames,
|
||||
flip_axis='horizontal',
|
||||
easing='ease_out',
|
||||
object_type='text',
|
||||
frame_width=frame_width,
|
||||
frame_height=frame_height,
|
||||
bg_color=(255, 255, 255)
|
||||
)
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == '__main__':
|
||||
print("Creating flip animations...")
|
||||
|
||||
builder = GIFBuilder(width=480, height=480, fps=20)
|
||||
|
||||
# Example 1: Emoji flip
|
||||
frames = create_flip_animation(
|
||||
object1_data={'emoji': '😊', 'size': 120},
|
||||
object2_data={'emoji': '😂', 'size': 120},
|
||||
num_frames=30,
|
||||
flip_axis='horizontal',
|
||||
object_type='emoji'
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('flip_emoji.gif', num_colors=128)
|
||||
|
||||
# Example 2: Text flip
|
||||
builder.clear()
|
||||
frames = create_flip_animation(
|
||||
object1_data={'text': 'YES', 'font_size': 80, 'text_color': (100, 200, 100)},
|
||||
object2_data={'text': 'NO', 'font_size': 80, 'text_color': (200, 100, 100)},
|
||||
num_frames=30,
|
||||
flip_axis='vertical',
|
||||
object_type='text'
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('flip_text.gif', num_colors=128)
|
||||
|
||||
# Example 3: Quick flip (emoji size)
|
||||
builder = GIFBuilder(width=128, height=128, fps=15)
|
||||
frames = create_quick_flip('👍', '👎', num_frames=20)
|
||||
builder.add_frames(frames)
|
||||
builder.save('flip_quick.gif', num_colors=48, optimize_for_emoji=True)
|
||||
|
||||
print("Created flip animations!")
|
||||
211
skills/slack-gif-creator/templates/kaleidoscope.py
Normal file
211
skills/slack-gif-creator/templates/kaleidoscope.py
Normal file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Kaleidoscope Effect - Create mirror/rotation effects.
|
||||
|
||||
Apply kaleidoscope effects to frames or objects for psychedelic visuals.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import math
|
||||
|
||||
sys.path.append(str(Path(__file__).parent.parent))
|
||||
|
||||
from PIL import Image, ImageOps, ImageDraw
|
||||
import numpy as np
|
||||
|
||||
|
||||
def apply_kaleidoscope(frame: Image.Image, segments: int = 8,
|
||||
center: tuple[int, int] | None = None) -> Image.Image:
|
||||
"""
|
||||
Apply kaleidoscope effect by mirroring/rotating frame sections.
|
||||
|
||||
Args:
|
||||
frame: Input frame
|
||||
segments: Number of mirror segments (4, 6, 8, 12 work well)
|
||||
center: Center point for effect (None = frame center)
|
||||
|
||||
Returns:
|
||||
Frame with kaleidoscope effect
|
||||
"""
|
||||
width, height = frame.size
|
||||
|
||||
if center is None:
|
||||
center = (width // 2, height // 2)
|
||||
|
||||
# Create output frame
|
||||
output = Image.new('RGB', (width, height))
|
||||
|
||||
# Calculate angle per segment
|
||||
angle_per_segment = 360 / segments
|
||||
|
||||
# For simplicity, we'll create a radial mirror effect
|
||||
# A full implementation would rotate and mirror properly
|
||||
# This is a simplified version that creates interesting patterns
|
||||
|
||||
# Convert to numpy for easier manipulation
|
||||
frame_array = np.array(frame)
|
||||
output_array = np.zeros_like(frame_array)
|
||||
|
||||
center_x, center_y = center
|
||||
|
||||
# Create wedge mask and mirror it
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
# Calculate angle from center
|
||||
dx = x - center_x
|
||||
dy = y - center_y
|
||||
|
||||
angle = (math.degrees(math.atan2(dy, dx)) + 180) % 360
|
||||
distance = math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
# Which segment does this pixel belong to?
|
||||
segment = int(angle / angle_per_segment)
|
||||
|
||||
# Mirror angle within segment
|
||||
segment_angle = angle % angle_per_segment
|
||||
if segment % 2 == 1: # Mirror every other segment
|
||||
segment_angle = angle_per_segment - segment_angle
|
||||
|
||||
# Calculate source position
|
||||
source_angle = segment_angle + (segment // 2) * angle_per_segment * 2
|
||||
source_angle_rad = math.radians(source_angle - 180)
|
||||
|
||||
source_x = int(center_x + distance * math.cos(source_angle_rad))
|
||||
source_y = int(center_y + distance * math.sin(source_angle_rad))
|
||||
|
||||
# Bounds check
|
||||
if 0 <= source_x < width and 0 <= source_y < height:
|
||||
output_array[y, x] = frame_array[source_y, source_x]
|
||||
else:
|
||||
output_array[y, x] = frame_array[y, x]
|
||||
|
||||
return Image.fromarray(output_array)
|
||||
|
||||
|
||||
def apply_simple_mirror(frame: Image.Image, mode: str = 'quad') -> Image.Image:
|
||||
"""
|
||||
Apply simple mirror effect (faster than full kaleidoscope).
|
||||
|
||||
Args:
|
||||
frame: Input frame
|
||||
mode: 'horizontal', 'vertical', 'quad' (4-way), 'radial'
|
||||
|
||||
Returns:
|
||||
Mirrored frame
|
||||
"""
|
||||
width, height = frame.size
|
||||
center_x, center_y = width // 2, height // 2
|
||||
|
||||
if mode == 'horizontal':
|
||||
# Mirror left half to right
|
||||
left_half = frame.crop((0, 0, center_x, height))
|
||||
left_flipped = ImageOps.mirror(left_half)
|
||||
result = frame.copy()
|
||||
result.paste(left_flipped, (center_x, 0))
|
||||
return result
|
||||
|
||||
elif mode == 'vertical':
|
||||
# Mirror top half to bottom
|
||||
top_half = frame.crop((0, 0, width, center_y))
|
||||
top_flipped = ImageOps.flip(top_half)
|
||||
result = frame.copy()
|
||||
result.paste(top_flipped, (0, center_y))
|
||||
return result
|
||||
|
||||
elif mode == 'quad':
|
||||
# 4-way mirror (top-left quadrant mirrored to all)
|
||||
quad = frame.crop((0, 0, center_x, center_y))
|
||||
|
||||
result = Image.new('RGB', (width, height))
|
||||
|
||||
# Top-left (original)
|
||||
result.paste(quad, (0, 0))
|
||||
|
||||
# Top-right (horizontal mirror)
|
||||
result.paste(ImageOps.mirror(quad), (center_x, 0))
|
||||
|
||||
# Bottom-left (vertical mirror)
|
||||
result.paste(ImageOps.flip(quad), (0, center_y))
|
||||
|
||||
# Bottom-right (both mirrors)
|
||||
result.paste(ImageOps.flip(ImageOps.mirror(quad)), (center_x, center_y))
|
||||
|
||||
return result
|
||||
|
||||
else:
|
||||
return frame
|
||||
|
||||
|
||||
def create_kaleidoscope_animation(
|
||||
base_frame: Image.Image | None = None,
|
||||
num_frames: int = 30,
|
||||
segments: int = 8,
|
||||
rotation_speed: float = 1.0,
|
||||
width: int = 480,
|
||||
height: int = 480
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create animated kaleidoscope effect.
|
||||
|
||||
Args:
|
||||
base_frame: Frame to apply effect to (or None for demo pattern)
|
||||
num_frames: Number of frames
|
||||
segments: Kaleidoscope segments
|
||||
rotation_speed: How fast pattern rotates (0.5-2.0)
|
||||
width: Frame width if generating demo
|
||||
height: Frame height if generating demo
|
||||
|
||||
Returns:
|
||||
List of frames with kaleidoscope effect
|
||||
"""
|
||||
frames = []
|
||||
|
||||
# Create demo pattern if no base frame
|
||||
if base_frame is None:
|
||||
base_frame = Image.new('RGB', (width, height), (255, 255, 255))
|
||||
draw = ImageDraw.Draw(base_frame)
|
||||
|
||||
# Draw some colored shapes
|
||||
from core.color_palettes import get_palette
|
||||
palette = get_palette('vibrant')
|
||||
|
||||
colors = [palette['primary'], palette['secondary'], palette['accent']]
|
||||
|
||||
for i, color in enumerate(colors):
|
||||
x = width // 2 + int(100 * math.cos(i * 2 * math.pi / 3))
|
||||
y = height // 2 + int(100 * math.sin(i * 2 * math.pi / 3))
|
||||
draw.ellipse([x - 40, y - 40, x + 40, y + 40], fill=color)
|
||||
|
||||
# Rotate base frame and apply kaleidoscope
|
||||
for i in range(num_frames):
|
||||
angle = (i / num_frames) * 360 * rotation_speed
|
||||
|
||||
# Rotate base frame
|
||||
rotated = base_frame.rotate(angle, resample=Image.BICUBIC)
|
||||
|
||||
# Apply kaleidoscope
|
||||
kaleido_frame = apply_kaleidoscope(rotated, segments=segments)
|
||||
|
||||
frames.append(kaleido_frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == '__main__':
|
||||
from core.gif_builder import GIFBuilder
|
||||
|
||||
print("Creating kaleidoscope GIF...")
|
||||
|
||||
builder = GIFBuilder(width=480, height=480, fps=20)
|
||||
|
||||
# Create kaleidoscope animation
|
||||
frames = create_kaleidoscope_animation(
|
||||
num_frames=40,
|
||||
segments=8,
|
||||
rotation_speed=0.5
|
||||
)
|
||||
|
||||
builder.add_frames(frames)
|
||||
builder.save('kaleidoscope_test.gif', num_colors=128)
|
||||
329
skills/slack-gif-creator/templates/morph.py
Normal file
329
skills/slack-gif-creator/templates/morph.py
Normal file
@@ -0,0 +1,329 @@
|
||||
#!/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!")
|
||||
293
skills/slack-gif-creator/templates/move.py
Normal file
293
skills/slack-gif-creator/templates/move.py
Normal 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!")
|
||||
268
skills/slack-gif-creator/templates/pulse.py
Normal file
268
skills/slack-gif-creator/templates/pulse.py
Normal file
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pulse Animation - Scale objects rhythmically for emphasis.
|
||||
|
||||
Creates pulsing, heartbeat, and throbbing 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_pulse_animation(
|
||||
object_type: str = 'emoji',
|
||||
object_data: dict | None = None,
|
||||
num_frames: int = 30,
|
||||
pulse_type: str = 'smooth', # 'smooth', 'heartbeat', 'throb', 'pop'
|
||||
scale_range: tuple[float, float] = (0.8, 1.2),
|
||||
pulses: 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 pulsing/scaling animation.
|
||||
|
||||
Args:
|
||||
object_type: 'emoji', 'circle', 'text'
|
||||
object_data: Object configuration
|
||||
num_frames: Number of frames
|
||||
pulse_type: Type of pulsing motion
|
||||
scale_range: (min_scale, max_scale) tuple
|
||||
pulses: Number of pulses in animation
|
||||
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}
|
||||
elif object_type == 'circle':
|
||||
object_data = {'radius': 50, 'color': (255, 100, 100)}
|
||||
|
||||
min_scale, max_scale = scale_range
|
||||
|
||||
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 scale based on pulse type
|
||||
if pulse_type == 'smooth':
|
||||
# Simple sinusoidal pulse
|
||||
scale = min_scale + (max_scale - min_scale) * (
|
||||
0.5 + 0.5 * math.sin(t * pulses * 2 * math.pi - math.pi / 2)
|
||||
)
|
||||
|
||||
elif pulse_type == 'heartbeat':
|
||||
# Double pump like a heartbeat
|
||||
phase = (t * pulses) % 1.0
|
||||
if phase < 0.15:
|
||||
# First pump
|
||||
scale = interpolate(min_scale, max_scale, phase / 0.15, 'ease_out')
|
||||
elif phase < 0.25:
|
||||
# First release
|
||||
scale = interpolate(max_scale, min_scale, (phase - 0.15) / 0.10, 'ease_in')
|
||||
elif phase < 0.35:
|
||||
# Second pump (smaller)
|
||||
scale = interpolate(min_scale, (min_scale + max_scale) / 2, (phase - 0.25) / 0.10, 'ease_out')
|
||||
elif phase < 0.45:
|
||||
# Second release
|
||||
scale = interpolate((min_scale + max_scale) / 2, min_scale, (phase - 0.35) / 0.10, 'ease_in')
|
||||
else:
|
||||
# Rest period
|
||||
scale = min_scale
|
||||
|
||||
elif pulse_type == 'throb':
|
||||
# Sharp pulse with quick return
|
||||
phase = (t * pulses) % 1.0
|
||||
if phase < 0.2:
|
||||
scale = interpolate(min_scale, max_scale, phase / 0.2, 'ease_out')
|
||||
else:
|
||||
scale = interpolate(max_scale, min_scale, (phase - 0.2) / 0.8, 'ease_in')
|
||||
|
||||
elif pulse_type == 'pop':
|
||||
# Pop out and back with overshoot
|
||||
phase = (t * pulses) % 1.0
|
||||
if phase < 0.3:
|
||||
# Pop out with overshoot
|
||||
scale = interpolate(min_scale, max_scale * 1.1, phase / 0.3, 'elastic_out')
|
||||
else:
|
||||
# Settle back
|
||||
scale = interpolate(max_scale * 1.1, min_scale, (phase - 0.3) / 0.7, 'ease_out')
|
||||
|
||||
else:
|
||||
scale = min_scale + (max_scale - min_scale) * (
|
||||
0.5 + 0.5 * math.sin(t * pulses * 2 * math.pi)
|
||||
)
|
||||
|
||||
# Draw object at calculated scale
|
||||
if object_type == 'emoji':
|
||||
base_size = object_data['size']
|
||||
current_size = int(base_size * scale)
|
||||
draw_emoji_enhanced(
|
||||
frame,
|
||||
emoji=object_data['emoji'],
|
||||
position=(center_pos[0] - current_size // 2, center_pos[1] - current_size // 2),
|
||||
size=current_size,
|
||||
shadow=object_data.get('shadow', True)
|
||||
)
|
||||
|
||||
elif object_type == 'circle':
|
||||
base_radius = object_data['radius']
|
||||
current_radius = int(base_radius * scale)
|
||||
draw_circle(
|
||||
frame,
|
||||
center=center_pos,
|
||||
radius=current_radius,
|
||||
fill_color=object_data['color']
|
||||
)
|
||||
|
||||
elif object_type == 'text':
|
||||
from core.typography import draw_text_with_outline
|
||||
base_size = object_data.get('font_size', 50)
|
||||
current_size = int(base_size * scale)
|
||||
draw_text_with_outline(
|
||||
frame,
|
||||
text=object_data.get('text', 'PULSE'),
|
||||
position=center_pos,
|
||||
font_size=current_size,
|
||||
text_color=object_data.get('text_color', (255, 100, 100)),
|
||||
outline_color=object_data.get('outline_color', (0, 0, 0)),
|
||||
outline_width=3,
|
||||
centered=True
|
||||
)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
def create_attention_pulse(
|
||||
emoji: str = '⚠️',
|
||||
num_frames: int = 20,
|
||||
frame_size: int = 128,
|
||||
bg_color: tuple[int, int, int] = (255, 255, 255)
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create attention-grabbing pulse (good for emoji GIFs).
|
||||
|
||||
Args:
|
||||
emoji: Emoji to pulse
|
||||
num_frames: Number of frames
|
||||
frame_size: Frame size (square)
|
||||
bg_color: Background color
|
||||
|
||||
Returns:
|
||||
List of frames optimized for emoji size
|
||||
"""
|
||||
return create_pulse_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': emoji, 'size': 80, 'shadow': False},
|
||||
num_frames=num_frames,
|
||||
pulse_type='throb',
|
||||
scale_range=(0.85, 1.15),
|
||||
pulses=2,
|
||||
center_pos=(frame_size // 2, frame_size // 2),
|
||||
frame_width=frame_size,
|
||||
frame_height=frame_size,
|
||||
bg_color=bg_color
|
||||
)
|
||||
|
||||
|
||||
def create_breathing_animation(
|
||||
object_type: str = 'emoji',
|
||||
object_data: dict | None = None,
|
||||
num_frames: int = 60,
|
||||
breaths: float = 2.0,
|
||||
scale_range: tuple[float, float] = (0.9, 1.1),
|
||||
frame_width: int = 480,
|
||||
frame_height: int = 480,
|
||||
bg_color: tuple[int, int, int] = (240, 248, 255)
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create slow, calming breathing animation (in and out).
|
||||
|
||||
Args:
|
||||
object_type: Type of object
|
||||
object_data: Object configuration
|
||||
num_frames: Number of frames
|
||||
breaths: Number of breathing cycles
|
||||
scale_range: Min/max scale
|
||||
frame_width: Frame width
|
||||
frame_height: Frame height
|
||||
bg_color: Background color
|
||||
|
||||
Returns:
|
||||
List of frames
|
||||
"""
|
||||
if object_data is None:
|
||||
object_data = {'emoji': '😌', 'size': 100}
|
||||
|
||||
return create_pulse_animation(
|
||||
object_type=object_type,
|
||||
object_data=object_data,
|
||||
num_frames=num_frames,
|
||||
pulse_type='smooth',
|
||||
scale_range=scale_range,
|
||||
pulses=breaths,
|
||||
center_pos=(frame_width // 2, frame_height // 2),
|
||||
frame_width=frame_width,
|
||||
frame_height=frame_height,
|
||||
bg_color=bg_color
|
||||
)
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == '__main__':
|
||||
print("Creating pulse animations...")
|
||||
|
||||
builder = GIFBuilder(width=480, height=480, fps=20)
|
||||
|
||||
# Example 1: Smooth pulse
|
||||
frames = create_pulse_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '❤️', 'size': 100},
|
||||
num_frames=40,
|
||||
pulse_type='smooth',
|
||||
scale_range=(0.8, 1.2),
|
||||
pulses=2
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('pulse_smooth.gif', num_colors=128)
|
||||
|
||||
# Example 2: Heartbeat
|
||||
builder.clear()
|
||||
frames = create_pulse_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '💓', 'size': 100},
|
||||
num_frames=60,
|
||||
pulse_type='heartbeat',
|
||||
scale_range=(0.85, 1.2),
|
||||
pulses=3
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('pulse_heartbeat.gif', num_colors=128)
|
||||
|
||||
# Example 3: Attention pulse (emoji size)
|
||||
builder = GIFBuilder(width=128, height=128, fps=15)
|
||||
frames = create_attention_pulse(emoji='⚠️', num_frames=20)
|
||||
builder.add_frames(frames)
|
||||
builder.save('pulse_attention.gif', num_colors=48, optimize_for_emoji=True)
|
||||
|
||||
print("Created pulse animations!")
|
||||
127
skills/slack-gif-creator/templates/shake.py
Normal file
127
skills/slack-gif-creator/templates/shake.py
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Shake Animation Template - Creates shaking/vibrating motion.
|
||||
|
||||
Use this for impact effects, emphasis, or nervous/excited reactions.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
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, draw_text
|
||||
from core.easing import ease_out_quad
|
||||
|
||||
|
||||
def create_shake_animation(
|
||||
object_type: str = 'emoji',
|
||||
object_data: dict = None,
|
||||
num_frames: int = 20,
|
||||
shake_intensity: int = 15,
|
||||
center_x: int = 240,
|
||||
center_y: int = 240,
|
||||
direction: str = 'horizontal', # 'horizontal', 'vertical', or 'both'
|
||||
frame_width: int = 480,
|
||||
frame_height: int = 480,
|
||||
bg_color: tuple[int, int, int] = (255, 255, 255)
|
||||
) -> list:
|
||||
"""
|
||||
Create frames for a shaking animation.
|
||||
|
||||
Args:
|
||||
object_type: 'circle', 'emoji', 'text', or 'custom'
|
||||
object_data: Data for the object
|
||||
num_frames: Number of frames
|
||||
shake_intensity: Maximum shake displacement in pixels
|
||||
center_x: Center X position
|
||||
center_y: Center Y position
|
||||
direction: 'horizontal', 'vertical', or 'both'
|
||||
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': 80}
|
||||
elif object_type == 'text':
|
||||
object_data = {'text': 'SHAKE!', 'font_size': 50, 'color': (255, 0, 0)}
|
||||
|
||||
for i in range(num_frames):
|
||||
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
||||
|
||||
# Calculate progress
|
||||
t = i / (num_frames - 1) if num_frames > 1 else 0
|
||||
|
||||
# Decay shake intensity over time
|
||||
intensity = shake_intensity * (1 - ease_out_quad(t))
|
||||
|
||||
# Calculate shake offset using sine wave for smooth oscillation
|
||||
freq = 3 # Oscillation frequency
|
||||
offset_x = 0
|
||||
offset_y = 0
|
||||
|
||||
if direction in ['horizontal', 'both']:
|
||||
offset_x = int(math.sin(t * freq * 2 * math.pi) * intensity)
|
||||
|
||||
if direction in ['vertical', 'both']:
|
||||
offset_y = int(math.cos(t * freq * 2 * math.pi) * intensity)
|
||||
|
||||
# Apply offset
|
||||
x = center_x + offset_x
|
||||
y = center_y + offset_y
|
||||
|
||||
# Draw object
|
||||
if object_type == 'emoji':
|
||||
draw_emoji(
|
||||
frame,
|
||||
emoji=object_data['emoji'],
|
||||
position=(x - object_data['size'] // 2, y - object_data['size'] // 2),
|
||||
size=object_data['size']
|
||||
)
|
||||
elif object_type == 'text':
|
||||
draw_text(
|
||||
frame,
|
||||
text=object_data['text'],
|
||||
position=(x, y),
|
||||
font_size=object_data['font_size'],
|
||||
color=object_data['color'],
|
||||
centered=True
|
||||
)
|
||||
elif object_type == 'circle':
|
||||
draw_circle(
|
||||
frame,
|
||||
center=(x, y),
|
||||
radius=object_data.get('radius', 30),
|
||||
fill_color=object_data.get('color', (100, 100, 255))
|
||||
)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == '__main__':
|
||||
print("Creating shake GIF...")
|
||||
|
||||
builder = GIFBuilder(width=480, height=480, fps=24)
|
||||
|
||||
frames = create_shake_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '😱', 'size': 100},
|
||||
num_frames=30,
|
||||
shake_intensity=20,
|
||||
direction='both'
|
||||
)
|
||||
|
||||
builder.add_frames(frames)
|
||||
builder.save('shake_test.gif', num_colors=128)
|
||||
291
skills/slack-gif-creator/templates/slide.py
Normal file
291
skills/slack-gif-creator/templates/slide.py
Normal file
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Slide Animation - Slide elements in from edges with overshoot/bounce.
|
||||
|
||||
Creates smooth entrance and exit animations.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
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_slide_animation(
|
||||
object_type: str = 'emoji',
|
||||
object_data: dict | None = None,
|
||||
num_frames: int = 30,
|
||||
direction: str = 'left', # 'left', 'right', 'top', 'bottom'
|
||||
slide_type: str = 'in', # 'in', 'out', 'across'
|
||||
easing: str = 'ease_out',
|
||||
overshoot: bool = False,
|
||||
final_pos: tuple[int, int] | None = None,
|
||||
frame_width: int = 480,
|
||||
frame_height: int = 480,
|
||||
bg_color: tuple[int, int, int] = (255, 255, 255)
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create slide animation.
|
||||
|
||||
Args:
|
||||
object_type: 'emoji', 'text'
|
||||
object_data: Object configuration
|
||||
num_frames: Number of frames
|
||||
direction: Direction of slide
|
||||
slide_type: Type of slide (in/out/across)
|
||||
easing: Easing function
|
||||
overshoot: Add overshoot/bounce at end
|
||||
final_pos: Final position (None = center)
|
||||
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}
|
||||
|
||||
if final_pos is None:
|
||||
final_pos = (frame_width // 2, frame_height // 2)
|
||||
|
||||
# Calculate start and end positions based on direction
|
||||
size = object_data.get('size', 100) if object_type == 'emoji' else 100
|
||||
margin = size
|
||||
|
||||
if direction == 'left':
|
||||
start_pos = (-margin, final_pos[1])
|
||||
end_pos = final_pos if slide_type == 'in' else (frame_width + margin, final_pos[1])
|
||||
elif direction == 'right':
|
||||
start_pos = (frame_width + margin, final_pos[1])
|
||||
end_pos = final_pos if slide_type == 'in' else (-margin, final_pos[1])
|
||||
elif direction == 'top':
|
||||
start_pos = (final_pos[0], -margin)
|
||||
end_pos = final_pos if slide_type == 'in' else (final_pos[0], frame_height + margin)
|
||||
elif direction == 'bottom':
|
||||
start_pos = (final_pos[0], frame_height + margin)
|
||||
end_pos = final_pos if slide_type == 'in' else (final_pos[0], -margin)
|
||||
else:
|
||||
start_pos = (-margin, final_pos[1])
|
||||
end_pos = final_pos
|
||||
|
||||
# For 'out' type, swap start and end
|
||||
if slide_type == 'out':
|
||||
start_pos, end_pos = final_pos, end_pos
|
||||
elif slide_type == 'across':
|
||||
# Slide all the way across
|
||||
if direction == 'left':
|
||||
start_pos = (-margin, final_pos[1])
|
||||
end_pos = (frame_width + margin, final_pos[1])
|
||||
elif direction == 'right':
|
||||
start_pos = (frame_width + margin, final_pos[1])
|
||||
end_pos = (-margin, final_pos[1])
|
||||
elif direction == 'top':
|
||||
start_pos = (final_pos[0], -margin)
|
||||
end_pos = (final_pos[0], frame_height + margin)
|
||||
elif direction == 'bottom':
|
||||
start_pos = (final_pos[0], frame_height + margin)
|
||||
end_pos = (final_pos[0], -margin)
|
||||
|
||||
# Use overshoot easing if requested
|
||||
if overshoot and slide_type == 'in':
|
||||
easing = 'back_out'
|
||||
|
||||
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 current position
|
||||
x = int(interpolate(start_pos[0], end_pos[0], t, easing))
|
||||
y = int(interpolate(start_pos[1], end_pos[1], t, easing))
|
||||
|
||||
# Draw object
|
||||
if object_type == 'emoji':
|
||||
size = object_data['size']
|
||||
draw_emoji_enhanced(
|
||||
frame,
|
||||
emoji=object_data['emoji'],
|
||||
position=(x - size // 2, y - size // 2),
|
||||
size=size,
|
||||
shadow=object_data.get('shadow', True)
|
||||
)
|
||||
|
||||
elif object_type == 'text':
|
||||
from core.typography import draw_text_with_outline
|
||||
draw_text_with_outline(
|
||||
frame,
|
||||
text=object_data.get('text', 'SLIDE'),
|
||||
position=(x, y),
|
||||
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
|
||||
)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
def create_multi_slide(
|
||||
objects: list[dict],
|
||||
num_frames: int = 30,
|
||||
stagger_delay: int = 3,
|
||||
frame_width: int = 480,
|
||||
frame_height: int = 480,
|
||||
bg_color: tuple[int, int, int] = (255, 255, 255)
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create animation with multiple objects sliding in sequence.
|
||||
|
||||
Args:
|
||||
objects: List of object configs with 'type', 'data', 'direction', 'final_pos'
|
||||
num_frames: Number of frames
|
||||
stagger_delay: Frames between each object starting
|
||||
frame_width: Frame width
|
||||
frame_height: Frame height
|
||||
bg_color: Background color
|
||||
|
||||
Returns:
|
||||
List of frames
|
||||
"""
|
||||
frames = []
|
||||
|
||||
for i in range(num_frames):
|
||||
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
||||
|
||||
for idx, obj in enumerate(objects):
|
||||
# Calculate when this object starts moving
|
||||
start_frame = idx * stagger_delay
|
||||
if i < start_frame:
|
||||
continue # Object hasn't started yet
|
||||
|
||||
# Calculate progress for this object
|
||||
obj_frame = i - start_frame
|
||||
obj_duration = num_frames - start_frame
|
||||
if obj_duration <= 0:
|
||||
continue
|
||||
|
||||
t = obj_frame / obj_duration
|
||||
|
||||
# Get object properties
|
||||
obj_type = obj.get('type', 'emoji')
|
||||
obj_data = obj.get('data', {'emoji': '➡️', 'size': 80})
|
||||
direction = obj.get('direction', 'left')
|
||||
final_pos = obj.get('final_pos', (frame_width // 2, frame_height // 2))
|
||||
easing = obj.get('easing', 'back_out')
|
||||
|
||||
# Calculate position
|
||||
size = obj_data.get('size', 80)
|
||||
margin = size
|
||||
|
||||
if direction == 'left':
|
||||
start_x = -margin
|
||||
end_x = final_pos[0]
|
||||
y = final_pos[1]
|
||||
elif direction == 'right':
|
||||
start_x = frame_width + margin
|
||||
end_x = final_pos[0]
|
||||
y = final_pos[1]
|
||||
elif direction == 'top':
|
||||
x = final_pos[0]
|
||||
start_y = -margin
|
||||
end_y = final_pos[1]
|
||||
elif direction == 'bottom':
|
||||
x = final_pos[0]
|
||||
start_y = frame_height + margin
|
||||
end_y = final_pos[1]
|
||||
else:
|
||||
start_x = -margin
|
||||
end_x = final_pos[0]
|
||||
y = final_pos[1]
|
||||
|
||||
# Interpolate position
|
||||
if direction in ['left', 'right']:
|
||||
x = int(interpolate(start_x, end_x, t, easing))
|
||||
else:
|
||||
y = int(interpolate(start_y, end_y, t, easing))
|
||||
|
||||
# Draw object
|
||||
if obj_type == 'emoji':
|
||||
draw_emoji_enhanced(
|
||||
frame,
|
||||
emoji=obj_data['emoji'],
|
||||
position=(x - size // 2, y - size // 2),
|
||||
size=size,
|
||||
shadow=False
|
||||
)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == '__main__':
|
||||
print("Creating slide animations...")
|
||||
|
||||
builder = GIFBuilder(width=480, height=480, fps=20)
|
||||
|
||||
# Example 1: Slide in from left with overshoot
|
||||
frames = create_slide_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '➡️', 'size': 100},
|
||||
num_frames=30,
|
||||
direction='left',
|
||||
slide_type='in',
|
||||
overshoot=True
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('slide_in_left.gif', num_colors=128)
|
||||
|
||||
# Example 2: Slide across
|
||||
builder.clear()
|
||||
frames = create_slide_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '🚀', 'size': 80},
|
||||
num_frames=40,
|
||||
direction='left',
|
||||
slide_type='across',
|
||||
easing='ease_in_out'
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('slide_across.gif', num_colors=128)
|
||||
|
||||
# Example 3: Multiple objects sliding in
|
||||
builder.clear()
|
||||
objects = [
|
||||
{
|
||||
'type': 'emoji',
|
||||
'data': {'emoji': '🎯', 'size': 60},
|
||||
'direction': 'left',
|
||||
'final_pos': (120, 240)
|
||||
},
|
||||
{
|
||||
'type': 'emoji',
|
||||
'data': {'emoji': '🎪', 'size': 60},
|
||||
'direction': 'right',
|
||||
'final_pos': (240, 240)
|
||||
},
|
||||
{
|
||||
'type': 'emoji',
|
||||
'data': {'emoji': '🎨', 'size': 60},
|
||||
'direction': 'top',
|
||||
'final_pos': (360, 240)
|
||||
}
|
||||
]
|
||||
frames = create_multi_slide(objects, num_frames=50, stagger_delay=5)
|
||||
builder.add_frames(frames)
|
||||
builder.save('slide_multi.gif', num_colors=128)
|
||||
|
||||
print("Created slide animations!")
|
||||
269
skills/slack-gif-creator/templates/spin.py
Normal file
269
skills/slack-gif-creator/templates/spin.py
Normal 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!")
|
||||
300
skills/slack-gif-creator/templates/wiggle.py
Normal file
300
skills/slack-gif-creator/templates/wiggle.py
Normal 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!")
|
||||
312
skills/slack-gif-creator/templates/zoom.py
Normal file
312
skills/slack-gif-creator/templates/zoom.py
Normal file
@@ -0,0 +1,312 @@
|
||||
#!/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!")
|
||||
Reference in New Issue
Block a user