1#!/usr/bin/env python3
  2"""
  3Pulse Animation - Scale objects rhythmically for emphasis.
  4
  5Creates pulsing, heartbeat, and throbbing effects.
  6"""
  7
  8import sys
  9from pathlib import Path
 10import math
 11
 12sys.path.append(str(Path(__file__).parent.parent))
 13
 14from PIL import Image
 15from core.gif_builder import GIFBuilder
 16from core.frame_composer import create_blank_frame, draw_emoji_enhanced, draw_circle
 17from core.easing import interpolate
 18
 19
 20def create_pulse_animation(
 21    object_type: str = 'emoji',
 22    object_data: dict | None = None,
 23    num_frames: int = 30,
 24    pulse_type: str = 'smooth',  # 'smooth', 'heartbeat', 'throb', 'pop'
 25    scale_range: tuple[float, float] = (0.8, 1.2),
 26    pulses: float = 2.0,
 27    center_pos: tuple[int, int] = (240, 240),
 28    frame_width: int = 480,
 29    frame_height: int = 480,
 30    bg_color: tuple[int, int, int] = (255, 255, 255)
 31) -> list[Image.Image]:
 32    """
 33    Create pulsing/scaling animation.
 34
 35    Args:
 36        object_type: 'emoji', 'circle', 'text'
 37        object_data: Object configuration
 38        num_frames: Number of frames
 39        pulse_type: Type of pulsing motion
 40        scale_range: (min_scale, max_scale) tuple
 41        pulses: Number of pulses in animation
 42        center_pos: Center position
 43        frame_width: Frame width
 44        frame_height: Frame height
 45        bg_color: Background color
 46
 47    Returns:
 48        List of frames
 49    """
 50    frames = []
 51
 52    # Default object data
 53    if object_data is None:
 54        if object_type == 'emoji':
 55            object_data = {'emoji': '❤️', 'size': 100}
 56        elif object_type == 'circle':
 57            object_data = {'radius': 50, 'color': (255, 100, 100)}
 58
 59    min_scale, max_scale = scale_range
 60
 61    for i in range(num_frames):
 62        frame = create_blank_frame(frame_width, frame_height, bg_color)
 63        t = i / (num_frames - 1) if num_frames > 1 else 0
 64
 65        # Calculate scale based on pulse type
 66        if pulse_type == 'smooth':
 67            # Simple sinusoidal pulse
 68            scale = min_scale + (max_scale - min_scale) * (
 69                0.5 + 0.5 * math.sin(t * pulses * 2 * math.pi - math.pi / 2)
 70            )
 71
 72        elif pulse_type == 'heartbeat':
 73            # Double pump like a heartbeat
 74            phase = (t * pulses) % 1.0
 75            if phase < 0.15:
 76                # First pump
 77                scale = interpolate(min_scale, max_scale, phase / 0.15, 'ease_out')
 78            elif phase < 0.25:
 79                # First release
 80                scale = interpolate(max_scale, min_scale, (phase - 0.15) / 0.10, 'ease_in')
 81            elif phase < 0.35:
 82                # Second pump (smaller)
 83                scale = interpolate(min_scale, (min_scale + max_scale) / 2, (phase - 0.25) / 0.10, 'ease_out')
 84            elif phase < 0.45:
 85                # Second release
 86                scale = interpolate((min_scale + max_scale) / 2, min_scale, (phase - 0.35) / 0.10, 'ease_in')
 87            else:
 88                # Rest period
 89                scale = min_scale
 90
 91        elif pulse_type == 'throb':
 92            # Sharp pulse with quick return
 93            phase = (t * pulses) % 1.0
 94            if phase < 0.2:
 95                scale = interpolate(min_scale, max_scale, phase / 0.2, 'ease_out')
 96            else:
 97                scale = interpolate(max_scale, min_scale, (phase - 0.2) / 0.8, 'ease_in')
 98
 99        elif pulse_type == 'pop':
100            # Pop out and back with overshoot
101            phase = (t * pulses) % 1.0
102            if phase < 0.3:
103                # Pop out with overshoot
104                scale = interpolate(min_scale, max_scale * 1.1, phase / 0.3, 'elastic_out')
105            else:
106                # Settle back
107                scale = interpolate(max_scale * 1.1, min_scale, (phase - 0.3) / 0.7, 'ease_out')
108
109        else:
110            scale = min_scale + (max_scale - min_scale) * (
111                0.5 + 0.5 * math.sin(t * pulses * 2 * math.pi)
112            )
113
114        # Draw object at calculated scale
115        if object_type == 'emoji':
116            base_size = object_data['size']
117            current_size = int(base_size * scale)
118            draw_emoji_enhanced(
119                frame,
120                emoji=object_data['emoji'],
121                position=(center_pos[0] - current_size // 2, center_pos[1] - current_size // 2),
122                size=current_size,
123                shadow=object_data.get('shadow', True)
124            )
125
126        elif object_type == 'circle':
127            base_radius = object_data['radius']
128            current_radius = int(base_radius * scale)
129            draw_circle(
130                frame,
131                center=center_pos,
132                radius=current_radius,
133                fill_color=object_data['color']
134            )
135
136        elif object_type == 'text':
137            from core.typography import draw_text_with_outline
138            base_size = object_data.get('font_size', 50)
139            current_size = int(base_size * scale)
140            draw_text_with_outline(
141                frame,
142                text=object_data.get('text', 'PULSE'),
143                position=center_pos,
144                font_size=current_size,
145                text_color=object_data.get('text_color', (255, 100, 100)),
146                outline_color=object_data.get('outline_color', (0, 0, 0)),
147                outline_width=3,
148                centered=True
149            )
150
151        frames.append(frame)
152
153    return frames
154
155
156def create_attention_pulse(
157    emoji: str = '⚠️',
158    num_frames: int = 20,
159    frame_size: int = 128,
160    bg_color: tuple[int, int, int] = (255, 255, 255)
161) -> list[Image.Image]:
162    """
163    Create attention-grabbing pulse (good for emoji GIFs).
164
165    Args:
166        emoji: Emoji to pulse
167        num_frames: Number of frames
168        frame_size: Frame size (square)
169        bg_color: Background color
170
171    Returns:
172        List of frames optimized for emoji size
173    """
174    return create_pulse_animation(
175        object_type='emoji',
176        object_data={'emoji': emoji, 'size': 80, 'shadow': False},
177        num_frames=num_frames,
178        pulse_type='throb',
179        scale_range=(0.85, 1.15),
180        pulses=2,
181        center_pos=(frame_size // 2, frame_size // 2),
182        frame_width=frame_size,
183        frame_height=frame_size,
184        bg_color=bg_color
185    )
186
187
188def create_breathing_animation(
189    object_type: str = 'emoji',
190    object_data: dict | None = None,
191    num_frames: int = 60,
192    breaths: float = 2.0,
193    scale_range: tuple[float, float] = (0.9, 1.1),
194    frame_width: int = 480,
195    frame_height: int = 480,
196    bg_color: tuple[int, int, int] = (240, 248, 255)
197) -> list[Image.Image]:
198    """
199    Create slow, calming breathing animation (in and out).
200
201    Args:
202        object_type: Type of object
203        object_data: Object configuration
204        num_frames: Number of frames
205        breaths: Number of breathing cycles
206        scale_range: Min/max scale
207        frame_width: Frame width
208        frame_height: Frame height
209        bg_color: Background color
210
211    Returns:
212        List of frames
213    """
214    if object_data is None:
215        object_data = {'emoji': '😌', 'size': 100}
216
217    return create_pulse_animation(
218        object_type=object_type,
219        object_data=object_data,
220        num_frames=num_frames,
221        pulse_type='smooth',
222        scale_range=scale_range,
223        pulses=breaths,
224        center_pos=(frame_width // 2, frame_height // 2),
225        frame_width=frame_width,
226        frame_height=frame_height,
227        bg_color=bg_color
228    )
229
230
231# Example usage
232if __name__ == '__main__':
233    print("Creating pulse animations...")
234
235    builder = GIFBuilder(width=480, height=480, fps=20)
236
237    # Example 1: Smooth pulse
238    frames = create_pulse_animation(
239        object_type='emoji',
240        object_data={'emoji': '❤️', 'size': 100},
241        num_frames=40,
242        pulse_type='smooth',
243        scale_range=(0.8, 1.2),
244        pulses=2
245    )
246    builder.add_frames(frames)
247    builder.save('pulse_smooth.gif', num_colors=128)
248
249    # Example 2: Heartbeat
250    builder.clear()
251    frames = create_pulse_animation(
252        object_type='emoji',
253        object_data={'emoji': '💓', 'size': 100},
254        num_frames=60,
255        pulse_type='heartbeat',
256        scale_range=(0.85, 1.2),
257        pulses=3
258    )
259    builder.add_frames(frames)
260    builder.save('pulse_heartbeat.gif', num_colors=128)
261
262    # Example 3: Attention pulse (emoji size)
263    builder = GIFBuilder(width=128, height=128, fps=15)
264    frames = create_attention_pulse(emoji='⚠️', num_frames=20)
265    builder.add_frames(frames)
266    builder.save('pulse_attention.gif', num_colors=48, optimize_for_emoji=True)
267
268    print("Created pulse animations!")