Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:03:29 +08:00
commit 374eecc4f3
158 changed files with 18863 additions and 0 deletions

View 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)

View 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!")

View 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!")

View 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!")

View 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)

View 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!")

View 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!")

View 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!")

View 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)

View 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!")

View 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!")

View 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!")

View 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!")