aico-cli 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/simple-config.mjs +41 -15
- package/dist/cli.mjs +35 -31
- package/package.json +4 -1
- package/templates/agents/aico/plan/function-point-analyzer.md +219 -0
- package/templates/agents/aico/requirement/test-crossplatform.ps1 +0 -0
- package/templates/agents/aico/requirement/test-crossplatform.sh +456 -0
- package/templates/commands/base//344/273/243/347/240/201/345/256/241/346/237/245/346/231/272/350/203/275/344/275/223.md +2 -5
- package/templates/commands/base//345/212/237/350/203/275/347/202/271/346/265/213/347/256/227.md +469 -19
- package/templates/skills/slack-gif-creator/LICENSE.txt +202 -0
- package/templates/skills/slack-gif-creator/SKILL.md +646 -0
- package/templates/skills/slack-gif-creator/core/color_palettes.py +302 -0
- package/templates/skills/slack-gif-creator/core/easing.py +230 -0
- package/templates/skills/slack-gif-creator/core/frame_composer.py +469 -0
- package/templates/skills/slack-gif-creator/core/gif_builder.py +246 -0
- package/templates/skills/slack-gif-creator/core/typography.py +357 -0
- package/templates/skills/slack-gif-creator/core/validators.py +264 -0
- package/templates/skills/slack-gif-creator/core/visual_effects.py +494 -0
- package/templates/skills/slack-gif-creator/requirements.txt +4 -0
- package/templates/skills/slack-gif-creator/templates/bounce.py +106 -0
- package/templates/skills/slack-gif-creator/templates/explode.py +331 -0
- package/templates/skills/slack-gif-creator/templates/fade.py +329 -0
- package/templates/skills/slack-gif-creator/templates/flip.py +291 -0
- package/templates/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
- package/templates/skills/slack-gif-creator/templates/morph.py +329 -0
- package/templates/skills/slack-gif-creator/templates/move.py +293 -0
- package/templates/skills/slack-gif-creator/templates/pulse.py +268 -0
- package/templates/skills/slack-gif-creator/templates/shake.py +127 -0
- package/templates/skills/slack-gif-creator/templates/slide.py +291 -0
- package/templates/skills/slack-gif-creator/templates/spin.py +269 -0
- package/templates/skills/slack-gif-creator/templates/wiggle.py +300 -0
- package/templates/skills/slack-gif-creator/templates/zoom.py +312 -0
- package/templates/skills/swimlane-diagram/README.md +373 -0
- package/templates/skills/swimlane-diagram/SKILL.md +242 -0
- package/templates/skills/swimlane-diagram/examples.md +405 -0
- package/templates/skills/swimlane-diagram/generators.mjs +258 -0
- package/templates/skills/swimlane-diagram/package.json +126 -0
- package/templates/skills/swimlane-diagram/reference.md +368 -0
- package/templates/skills/swimlane-diagram/swimlane-diagram.mjs +215 -0
- package/templates/skills/swimlane-diagram/swimlane-diagram.test.mjs +358 -0
- package/templates/skills/swimlane-diagram/validators.mjs +291 -0
- package/templates/skills/theme-factory/LICENSE.txt +202 -0
- package/templates/skills/theme-factory/SKILL.md +59 -0
- package/templates/skills/theme-factory/theme-showcase.pdf +0 -0
- package/templates/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/templates/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/templates/skills/theme-factory/themes/desert-rose.md +19 -0
- package/templates/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/templates/skills/theme-factory/themes/golden-hour.md +19 -0
- package/templates/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/templates/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/templates/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/templates/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/templates/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/templates/code.md +0 -70
- package/templates/windows-bootstrap.ps1 +0 -390
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Pulse Animation - Scale objects rhythmically for emphasis.
|
|
4
|
+
|
|
5
|
+
Creates pulsing, heartbeat, and throbbing effects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
sys.path.append(str(Path(__file__).parent.parent))
|
|
13
|
+
|
|
14
|
+
from PIL import Image
|
|
15
|
+
from core.gif_builder import GIFBuilder
|
|
16
|
+
from core.frame_composer import create_blank_frame, draw_emoji_enhanced, draw_circle
|
|
17
|
+
from core.easing import interpolate
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def 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
|
+
|
|
156
|
+
def 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
|
+
|
|
188
|
+
def 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
|
|
232
|
+
if __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!")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Shake Animation Template - Creates shaking/vibrating motion.
|
|
4
|
+
|
|
5
|
+
Use this for impact effects, emphasis, or nervous/excited reactions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import math
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
sys.path.append(str(Path(__file__).parent.parent))
|
|
13
|
+
|
|
14
|
+
from core.gif_builder import GIFBuilder
|
|
15
|
+
from core.frame_composer import create_blank_frame, draw_circle, draw_emoji, draw_text
|
|
16
|
+
from core.easing import ease_out_quad
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_shake_animation(
|
|
20
|
+
object_type: str = 'emoji',
|
|
21
|
+
object_data: dict = None,
|
|
22
|
+
num_frames: int = 20,
|
|
23
|
+
shake_intensity: int = 15,
|
|
24
|
+
center_x: int = 240,
|
|
25
|
+
center_y: int = 240,
|
|
26
|
+
direction: str = 'horizontal', # 'horizontal', 'vertical', or 'both'
|
|
27
|
+
frame_width: int = 480,
|
|
28
|
+
frame_height: int = 480,
|
|
29
|
+
bg_color: tuple[int, int, int] = (255, 255, 255)
|
|
30
|
+
) -> list:
|
|
31
|
+
"""
|
|
32
|
+
Create frames for a shaking animation.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
object_type: 'circle', 'emoji', 'text', or 'custom'
|
|
36
|
+
object_data: Data for the object
|
|
37
|
+
num_frames: Number of frames
|
|
38
|
+
shake_intensity: Maximum shake displacement in pixels
|
|
39
|
+
center_x: Center X position
|
|
40
|
+
center_y: Center Y position
|
|
41
|
+
direction: 'horizontal', 'vertical', or 'both'
|
|
42
|
+
frame_width: Frame width
|
|
43
|
+
frame_height: Frame height
|
|
44
|
+
bg_color: Background color
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
List of frames
|
|
48
|
+
"""
|
|
49
|
+
frames = []
|
|
50
|
+
|
|
51
|
+
# Default object data
|
|
52
|
+
if object_data is None:
|
|
53
|
+
if object_type == 'emoji':
|
|
54
|
+
object_data = {'emoji': '😱', 'size': 80}
|
|
55
|
+
elif object_type == 'text':
|
|
56
|
+
object_data = {'text': 'SHAKE!', 'font_size': 50, 'color': (255, 0, 0)}
|
|
57
|
+
|
|
58
|
+
for i in range(num_frames):
|
|
59
|
+
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
60
|
+
|
|
61
|
+
# Calculate progress
|
|
62
|
+
t = i / (num_frames - 1) if num_frames > 1 else 0
|
|
63
|
+
|
|
64
|
+
# Decay shake intensity over time
|
|
65
|
+
intensity = shake_intensity * (1 - ease_out_quad(t))
|
|
66
|
+
|
|
67
|
+
# Calculate shake offset using sine wave for smooth oscillation
|
|
68
|
+
freq = 3 # Oscillation frequency
|
|
69
|
+
offset_x = 0
|
|
70
|
+
offset_y = 0
|
|
71
|
+
|
|
72
|
+
if direction in ['horizontal', 'both']:
|
|
73
|
+
offset_x = int(math.sin(t * freq * 2 * math.pi) * intensity)
|
|
74
|
+
|
|
75
|
+
if direction in ['vertical', 'both']:
|
|
76
|
+
offset_y = int(math.cos(t * freq * 2 * math.pi) * intensity)
|
|
77
|
+
|
|
78
|
+
# Apply offset
|
|
79
|
+
x = center_x + offset_x
|
|
80
|
+
y = center_y + offset_y
|
|
81
|
+
|
|
82
|
+
# Draw object
|
|
83
|
+
if object_type == 'emoji':
|
|
84
|
+
draw_emoji(
|
|
85
|
+
frame,
|
|
86
|
+
emoji=object_data['emoji'],
|
|
87
|
+
position=(x - object_data['size'] // 2, y - object_data['size'] // 2),
|
|
88
|
+
size=object_data['size']
|
|
89
|
+
)
|
|
90
|
+
elif object_type == 'text':
|
|
91
|
+
draw_text(
|
|
92
|
+
frame,
|
|
93
|
+
text=object_data['text'],
|
|
94
|
+
position=(x, y),
|
|
95
|
+
font_size=object_data['font_size'],
|
|
96
|
+
color=object_data['color'],
|
|
97
|
+
centered=True
|
|
98
|
+
)
|
|
99
|
+
elif object_type == 'circle':
|
|
100
|
+
draw_circle(
|
|
101
|
+
frame,
|
|
102
|
+
center=(x, y),
|
|
103
|
+
radius=object_data.get('radius', 30),
|
|
104
|
+
fill_color=object_data.get('color', (100, 100, 255))
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
frames.append(frame)
|
|
108
|
+
|
|
109
|
+
return frames
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Example usage
|
|
113
|
+
if __name__ == '__main__':
|
|
114
|
+
print("Creating shake GIF...")
|
|
115
|
+
|
|
116
|
+
builder = GIFBuilder(width=480, height=480, fps=24)
|
|
117
|
+
|
|
118
|
+
frames = create_shake_animation(
|
|
119
|
+
object_type='emoji',
|
|
120
|
+
object_data={'emoji': '😱', 'size': 100},
|
|
121
|
+
num_frames=30,
|
|
122
|
+
shake_intensity=20,
|
|
123
|
+
direction='both'
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
builder.add_frames(frames)
|
|
127
|
+
builder.save('shake_test.gif', num_colors=128)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Slide Animation - Slide elements in from edges with overshoot/bounce.
|
|
4
|
+
|
|
5
|
+
Creates smooth entrance and exit animations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
sys.path.append(str(Path(__file__).parent.parent))
|
|
12
|
+
|
|
13
|
+
from PIL import Image
|
|
14
|
+
from core.gif_builder import GIFBuilder
|
|
15
|
+
from core.frame_composer import create_blank_frame, draw_emoji_enhanced
|
|
16
|
+
from core.easing import interpolate
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def 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
|
+
|
|
140
|
+
def 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
|
|
235
|
+
if __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!")
|