main
1#!/usr/bin/env python3
2"""
3Slide Animation - Slide elements in from edges with overshoot/bounce.
4
5Creates smooth entrance and exit animations.
6"""
7
8import sys
9from pathlib import Path
10
11sys.path.append(str(Path(__file__).parent.parent))
12
13from PIL import Image
14from core.gif_builder import GIFBuilder
15from core.frame_composer import create_blank_frame, draw_emoji_enhanced
16from core.easing import interpolate
17
18
19def create_slide_animation(
20 object_type: str = 'emoji',
21 object_data: dict | None = None,
22 num_frames: int = 30,
23 direction: str = 'left', # 'left', 'right', 'top', 'bottom'
24 slide_type: str = 'in', # 'in', 'out', 'across'
25 easing: str = 'ease_out',
26 overshoot: bool = False,
27 final_pos: tuple[int, int] | None = None,
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 slide animation.
34
35 Args:
36 object_type: 'emoji', 'text'
37 object_data: Object configuration
38 num_frames: Number of frames
39 direction: Direction of slide
40 slide_type: Type of slide (in/out/across)
41 easing: Easing function
42 overshoot: Add overshoot/bounce at end
43 final_pos: Final position (None = center)
44 frame_width: Frame width
45 frame_height: Frame height
46 bg_color: Background color
47
48 Returns:
49 List of frames
50 """
51 frames = []
52
53 # Default object data
54 if object_data is None:
55 if object_type == 'emoji':
56 object_data = {'emoji': '➡️', 'size': 100}
57
58 if final_pos is None:
59 final_pos = (frame_width // 2, frame_height // 2)
60
61 # Calculate start and end positions based on direction
62 size = object_data.get('size', 100) if object_type == 'emoji' else 100
63 margin = size
64
65 if direction == 'left':
66 start_pos = (-margin, final_pos[1])
67 end_pos = final_pos if slide_type == 'in' else (frame_width + margin, final_pos[1])
68 elif direction == 'right':
69 start_pos = (frame_width + margin, final_pos[1])
70 end_pos = final_pos if slide_type == 'in' else (-margin, final_pos[1])
71 elif direction == 'top':
72 start_pos = (final_pos[0], -margin)
73 end_pos = final_pos if slide_type == 'in' else (final_pos[0], frame_height + margin)
74 elif direction == 'bottom':
75 start_pos = (final_pos[0], frame_height + margin)
76 end_pos = final_pos if slide_type == 'in' else (final_pos[0], -margin)
77 else:
78 start_pos = (-margin, final_pos[1])
79 end_pos = final_pos
80
81 # For 'out' type, swap start and end
82 if slide_type == 'out':
83 start_pos, end_pos = final_pos, end_pos
84 elif slide_type == 'across':
85 # Slide all the way across
86 if direction == 'left':
87 start_pos = (-margin, final_pos[1])
88 end_pos = (frame_width + margin, final_pos[1])
89 elif direction == 'right':
90 start_pos = (frame_width + margin, final_pos[1])
91 end_pos = (-margin, final_pos[1])
92 elif direction == 'top':
93 start_pos = (final_pos[0], -margin)
94 end_pos = (final_pos[0], frame_height + margin)
95 elif direction == 'bottom':
96 start_pos = (final_pos[0], frame_height + margin)
97 end_pos = (final_pos[0], -margin)
98
99 # Use overshoot easing if requested
100 if overshoot and slide_type == 'in':
101 easing = 'back_out'
102
103 for i in range(num_frames):
104 t = i / (num_frames - 1) if num_frames > 1 else 0
105 frame = create_blank_frame(frame_width, frame_height, bg_color)
106
107 # Calculate current position
108 x = int(interpolate(start_pos[0], end_pos[0], t, easing))
109 y = int(interpolate(start_pos[1], end_pos[1], t, easing))
110
111 # Draw object
112 if object_type == 'emoji':
113 size = object_data['size']
114 draw_emoji_enhanced(
115 frame,
116 emoji=object_data['emoji'],
117 position=(x - size // 2, y - size // 2),
118 size=size,
119 shadow=object_data.get('shadow', True)
120 )
121
122 elif object_type == 'text':
123 from core.typography import draw_text_with_outline
124 draw_text_with_outline(
125 frame,
126 text=object_data.get('text', 'SLIDE'),
127 position=(x, y),
128 font_size=object_data.get('font_size', 50),
129 text_color=object_data.get('text_color', (0, 0, 0)),
130 outline_color=object_data.get('outline_color', (255, 255, 255)),
131 outline_width=3,
132 centered=True
133 )
134
135 frames.append(frame)
136
137 return frames
138
139
140def create_multi_slide(
141 objects: list[dict],
142 num_frames: int = 30,
143 stagger_delay: int = 3,
144 frame_width: int = 480,
145 frame_height: int = 480,
146 bg_color: tuple[int, int, int] = (255, 255, 255)
147) -> list[Image.Image]:
148 """
149 Create animation with multiple objects sliding in sequence.
150
151 Args:
152 objects: List of object configs with 'type', 'data', 'direction', 'final_pos'
153 num_frames: Number of frames
154 stagger_delay: Frames between each object starting
155 frame_width: Frame width
156 frame_height: Frame height
157 bg_color: Background color
158
159 Returns:
160 List of frames
161 """
162 frames = []
163
164 for i in range(num_frames):
165 frame = create_blank_frame(frame_width, frame_height, bg_color)
166
167 for idx, obj in enumerate(objects):
168 # Calculate when this object starts moving
169 start_frame = idx * stagger_delay
170 if i < start_frame:
171 continue # Object hasn't started yet
172
173 # Calculate progress for this object
174 obj_frame = i - start_frame
175 obj_duration = num_frames - start_frame
176 if obj_duration <= 0:
177 continue
178
179 t = obj_frame / obj_duration
180
181 # Get object properties
182 obj_type = obj.get('type', 'emoji')
183 obj_data = obj.get('data', {'emoji': '➡️', 'size': 80})
184 direction = obj.get('direction', 'left')
185 final_pos = obj.get('final_pos', (frame_width // 2, frame_height // 2))
186 easing = obj.get('easing', 'back_out')
187
188 # Calculate position
189 size = obj_data.get('size', 80)
190 margin = size
191
192 if direction == 'left':
193 start_x = -margin
194 end_x = final_pos[0]
195 y = final_pos[1]
196 elif direction == 'right':
197 start_x = frame_width + margin
198 end_x = final_pos[0]
199 y = final_pos[1]
200 elif direction == 'top':
201 x = final_pos[0]
202 start_y = -margin
203 end_y = final_pos[1]
204 elif direction == 'bottom':
205 x = final_pos[0]
206 start_y = frame_height + margin
207 end_y = final_pos[1]
208 else:
209 start_x = -margin
210 end_x = final_pos[0]
211 y = final_pos[1]
212
213 # Interpolate position
214 if direction in ['left', 'right']:
215 x = int(interpolate(start_x, end_x, t, easing))
216 else:
217 y = int(interpolate(start_y, end_y, t, easing))
218
219 # Draw object
220 if obj_type == 'emoji':
221 draw_emoji_enhanced(
222 frame,
223 emoji=obj_data['emoji'],
224 position=(x - size // 2, y - size // 2),
225 size=size,
226 shadow=False
227 )
228
229 frames.append(frame)
230
231 return frames
232
233
234# Example usage
235if __name__ == '__main__':
236 print("Creating slide animations...")
237
238 builder = GIFBuilder(width=480, height=480, fps=20)
239
240 # Example 1: Slide in from left with overshoot
241 frames = create_slide_animation(
242 object_type='emoji',
243 object_data={'emoji': '➡️', 'size': 100},
244 num_frames=30,
245 direction='left',
246 slide_type='in',
247 overshoot=True
248 )
249 builder.add_frames(frames)
250 builder.save('slide_in_left.gif', num_colors=128)
251
252 # Example 2: Slide across
253 builder.clear()
254 frames = create_slide_animation(
255 object_type='emoji',
256 object_data={'emoji': '🚀', 'size': 80},
257 num_frames=40,
258 direction='left',
259 slide_type='across',
260 easing='ease_in_out'
261 )
262 builder.add_frames(frames)
263 builder.save('slide_across.gif', num_colors=128)
264
265 # Example 3: Multiple objects sliding in
266 builder.clear()
267 objects = [
268 {
269 'type': 'emoji',
270 'data': {'emoji': '🎯', 'size': 60},
271 'direction': 'left',
272 'final_pos': (120, 240)
273 },
274 {
275 'type': 'emoji',
276 'data': {'emoji': '🎪', 'size': 60},
277 'direction': 'right',
278 'final_pos': (240, 240)
279 },
280 {
281 'type': 'emoji',
282 'data': {'emoji': '🎨', 'size': 60},
283 'direction': 'top',
284 'final_pos': (360, 240)
285 }
286 ]
287 frames = create_multi_slide(objects, num_frames=50, stagger_delay=5)
288 builder.add_frames(frames)
289 builder.save('slide_multi.gif', num_colors=128)
290
291 print("Created slide animations!")