Initial commit
This commit is contained in:
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!")
|
||||
Reference in New Issue
Block a user