Initial commit
This commit is contained in:
268
skills/slack-gif-creator/templates/pulse.py
Normal file
268
skills/slack-gif-creator/templates/pulse.py
Normal file
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pulse Animation - Scale objects rhythmically for emphasis.
|
||||
|
||||
Creates pulsing, heartbeat, and throbbing effects.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import math
|
||||
|
||||
sys.path.append(str(Path(__file__).parent.parent))
|
||||
|
||||
from PIL import Image
|
||||
from core.gif_builder import GIFBuilder
|
||||
from core.frame_composer import create_blank_frame, draw_emoji_enhanced, draw_circle
|
||||
from core.easing import interpolate
|
||||
|
||||
|
||||
def create_pulse_animation(
|
||||
object_type: str = 'emoji',
|
||||
object_data: dict | None = None,
|
||||
num_frames: int = 30,
|
||||
pulse_type: str = 'smooth', # 'smooth', 'heartbeat', 'throb', 'pop'
|
||||
scale_range: tuple[float, float] = (0.8, 1.2),
|
||||
pulses: float = 2.0,
|
||||
center_pos: tuple[int, int] = (240, 240),
|
||||
frame_width: int = 480,
|
||||
frame_height: int = 480,
|
||||
bg_color: tuple[int, int, int] = (255, 255, 255)
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create pulsing/scaling animation.
|
||||
|
||||
Args:
|
||||
object_type: 'emoji', 'circle', 'text'
|
||||
object_data: Object configuration
|
||||
num_frames: Number of frames
|
||||
pulse_type: Type of pulsing motion
|
||||
scale_range: (min_scale, max_scale) tuple
|
||||
pulses: Number of pulses in animation
|
||||
center_pos: Center position
|
||||
frame_width: Frame width
|
||||
frame_height: Frame height
|
||||
bg_color: Background color
|
||||
|
||||
Returns:
|
||||
List of frames
|
||||
"""
|
||||
frames = []
|
||||
|
||||
# Default object data
|
||||
if object_data is None:
|
||||
if object_type == 'emoji':
|
||||
object_data = {'emoji': '❤️', 'size': 100}
|
||||
elif object_type == 'circle':
|
||||
object_data = {'radius': 50, 'color': (255, 100, 100)}
|
||||
|
||||
min_scale, max_scale = scale_range
|
||||
|
||||
for i in range(num_frames):
|
||||
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
||||
t = i / (num_frames - 1) if num_frames > 1 else 0
|
||||
|
||||
# Calculate scale based on pulse type
|
||||
if pulse_type == 'smooth':
|
||||
# Simple sinusoidal pulse
|
||||
scale = min_scale + (max_scale - min_scale) * (
|
||||
0.5 + 0.5 * math.sin(t * pulses * 2 * math.pi - math.pi / 2)
|
||||
)
|
||||
|
||||
elif pulse_type == 'heartbeat':
|
||||
# Double pump like a heartbeat
|
||||
phase = (t * pulses) % 1.0
|
||||
if phase < 0.15:
|
||||
# First pump
|
||||
scale = interpolate(min_scale, max_scale, phase / 0.15, 'ease_out')
|
||||
elif phase < 0.25:
|
||||
# First release
|
||||
scale = interpolate(max_scale, min_scale, (phase - 0.15) / 0.10, 'ease_in')
|
||||
elif phase < 0.35:
|
||||
# Second pump (smaller)
|
||||
scale = interpolate(min_scale, (min_scale + max_scale) / 2, (phase - 0.25) / 0.10, 'ease_out')
|
||||
elif phase < 0.45:
|
||||
# Second release
|
||||
scale = interpolate((min_scale + max_scale) / 2, min_scale, (phase - 0.35) / 0.10, 'ease_in')
|
||||
else:
|
||||
# Rest period
|
||||
scale = min_scale
|
||||
|
||||
elif pulse_type == 'throb':
|
||||
# Sharp pulse with quick return
|
||||
phase = (t * pulses) % 1.0
|
||||
if phase < 0.2:
|
||||
scale = interpolate(min_scale, max_scale, phase / 0.2, 'ease_out')
|
||||
else:
|
||||
scale = interpolate(max_scale, min_scale, (phase - 0.2) / 0.8, 'ease_in')
|
||||
|
||||
elif pulse_type == 'pop':
|
||||
# Pop out and back with overshoot
|
||||
phase = (t * pulses) % 1.0
|
||||
if phase < 0.3:
|
||||
# Pop out with overshoot
|
||||
scale = interpolate(min_scale, max_scale * 1.1, phase / 0.3, 'elastic_out')
|
||||
else:
|
||||
# Settle back
|
||||
scale = interpolate(max_scale * 1.1, min_scale, (phase - 0.3) / 0.7, 'ease_out')
|
||||
|
||||
else:
|
||||
scale = min_scale + (max_scale - min_scale) * (
|
||||
0.5 + 0.5 * math.sin(t * pulses * 2 * math.pi)
|
||||
)
|
||||
|
||||
# Draw object at calculated scale
|
||||
if object_type == 'emoji':
|
||||
base_size = object_data['size']
|
||||
current_size = int(base_size * scale)
|
||||
draw_emoji_enhanced(
|
||||
frame,
|
||||
emoji=object_data['emoji'],
|
||||
position=(center_pos[0] - current_size // 2, center_pos[1] - current_size // 2),
|
||||
size=current_size,
|
||||
shadow=object_data.get('shadow', True)
|
||||
)
|
||||
|
||||
elif object_type == 'circle':
|
||||
base_radius = object_data['radius']
|
||||
current_radius = int(base_radius * scale)
|
||||
draw_circle(
|
||||
frame,
|
||||
center=center_pos,
|
||||
radius=current_radius,
|
||||
fill_color=object_data['color']
|
||||
)
|
||||
|
||||
elif object_type == 'text':
|
||||
from core.typography import draw_text_with_outline
|
||||
base_size = object_data.get('font_size', 50)
|
||||
current_size = int(base_size * scale)
|
||||
draw_text_with_outline(
|
||||
frame,
|
||||
text=object_data.get('text', 'PULSE'),
|
||||
position=center_pos,
|
||||
font_size=current_size,
|
||||
text_color=object_data.get('text_color', (255, 100, 100)),
|
||||
outline_color=object_data.get('outline_color', (0, 0, 0)),
|
||||
outline_width=3,
|
||||
centered=True
|
||||
)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
|
||||
|
||||
def create_attention_pulse(
|
||||
emoji: str = '⚠️',
|
||||
num_frames: int = 20,
|
||||
frame_size: int = 128,
|
||||
bg_color: tuple[int, int, int] = (255, 255, 255)
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create attention-grabbing pulse (good for emoji GIFs).
|
||||
|
||||
Args:
|
||||
emoji: Emoji to pulse
|
||||
num_frames: Number of frames
|
||||
frame_size: Frame size (square)
|
||||
bg_color: Background color
|
||||
|
||||
Returns:
|
||||
List of frames optimized for emoji size
|
||||
"""
|
||||
return create_pulse_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': emoji, 'size': 80, 'shadow': False},
|
||||
num_frames=num_frames,
|
||||
pulse_type='throb',
|
||||
scale_range=(0.85, 1.15),
|
||||
pulses=2,
|
||||
center_pos=(frame_size // 2, frame_size // 2),
|
||||
frame_width=frame_size,
|
||||
frame_height=frame_size,
|
||||
bg_color=bg_color
|
||||
)
|
||||
|
||||
|
||||
def create_breathing_animation(
|
||||
object_type: str = 'emoji',
|
||||
object_data: dict | None = None,
|
||||
num_frames: int = 60,
|
||||
breaths: float = 2.0,
|
||||
scale_range: tuple[float, float] = (0.9, 1.1),
|
||||
frame_width: int = 480,
|
||||
frame_height: int = 480,
|
||||
bg_color: tuple[int, int, int] = (240, 248, 255)
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
Create slow, calming breathing animation (in and out).
|
||||
|
||||
Args:
|
||||
object_type: Type of object
|
||||
object_data: Object configuration
|
||||
num_frames: Number of frames
|
||||
breaths: Number of breathing cycles
|
||||
scale_range: Min/max scale
|
||||
frame_width: Frame width
|
||||
frame_height: Frame height
|
||||
bg_color: Background color
|
||||
|
||||
Returns:
|
||||
List of frames
|
||||
"""
|
||||
if object_data is None:
|
||||
object_data = {'emoji': '😌', 'size': 100}
|
||||
|
||||
return create_pulse_animation(
|
||||
object_type=object_type,
|
||||
object_data=object_data,
|
||||
num_frames=num_frames,
|
||||
pulse_type='smooth',
|
||||
scale_range=scale_range,
|
||||
pulses=breaths,
|
||||
center_pos=(frame_width // 2, frame_height // 2),
|
||||
frame_width=frame_width,
|
||||
frame_height=frame_height,
|
||||
bg_color=bg_color
|
||||
)
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == '__main__':
|
||||
print("Creating pulse animations...")
|
||||
|
||||
builder = GIFBuilder(width=480, height=480, fps=20)
|
||||
|
||||
# Example 1: Smooth pulse
|
||||
frames = create_pulse_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '❤️', 'size': 100},
|
||||
num_frames=40,
|
||||
pulse_type='smooth',
|
||||
scale_range=(0.8, 1.2),
|
||||
pulses=2
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('pulse_smooth.gif', num_colors=128)
|
||||
|
||||
# Example 2: Heartbeat
|
||||
builder.clear()
|
||||
frames = create_pulse_animation(
|
||||
object_type='emoji',
|
||||
object_data={'emoji': '💓', 'size': 100},
|
||||
num_frames=60,
|
||||
pulse_type='heartbeat',
|
||||
scale_range=(0.85, 1.2),
|
||||
pulses=3
|
||||
)
|
||||
builder.add_frames(frames)
|
||||
builder.save('pulse_heartbeat.gif', num_colors=128)
|
||||
|
||||
# Example 3: Attention pulse (emoji size)
|
||||
builder = GIFBuilder(width=128, height=128, fps=15)
|
||||
frames = create_attention_pulse(emoji='⚠️', num_frames=20)
|
||||
builder.add_frames(frames)
|
||||
builder.save('pulse_attention.gif', num_colors=48, optimize_for_emoji=True)
|
||||
|
||||
print("Created pulse animations!")
|
||||
Reference in New Issue
Block a user