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,329 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Morph Animation - Transform between different emojis or shapes.
|
|
4
|
+
|
|
5
|
+
Creates smooth transitions and transformations.
|
|
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
|
+
import numpy as np
|
|
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_morph_animation(
|
|
21
|
+
object1_data: dict,
|
|
22
|
+
object2_data: dict,
|
|
23
|
+
num_frames: int = 30,
|
|
24
|
+
morph_type: str = 'crossfade', # 'crossfade', 'scale', 'spin_morph'
|
|
25
|
+
easing: str = 'ease_in_out',
|
|
26
|
+
object_type: str = 'emoji',
|
|
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 morphing animation between two objects.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
object1_data: First object configuration
|
|
37
|
+
object2_data: Second object configuration
|
|
38
|
+
num_frames: Number of frames
|
|
39
|
+
morph_type: Type of morph effect
|
|
40
|
+
easing: Easing function
|
|
41
|
+
object_type: Type of objects
|
|
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
|
+
for i in range(num_frames):
|
|
53
|
+
t = i / (num_frames - 1) if num_frames > 1 else 0
|
|
54
|
+
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
55
|
+
|
|
56
|
+
if morph_type == 'crossfade':
|
|
57
|
+
# Simple crossfade between two objects
|
|
58
|
+
opacity1 = interpolate(1, 0, t, easing)
|
|
59
|
+
opacity2 = interpolate(0, 1, t, easing)
|
|
60
|
+
|
|
61
|
+
if object_type == 'emoji':
|
|
62
|
+
# Create first emoji
|
|
63
|
+
emoji1_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
|
64
|
+
size1 = object1_data['size']
|
|
65
|
+
draw_emoji_enhanced(
|
|
66
|
+
emoji1_canvas,
|
|
67
|
+
emoji=object1_data['emoji'],
|
|
68
|
+
position=(center_pos[0] - size1 // 2, center_pos[1] - size1 // 2),
|
|
69
|
+
size=size1,
|
|
70
|
+
shadow=False
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Apply opacity
|
|
74
|
+
from templates.fade import apply_opacity
|
|
75
|
+
emoji1_canvas = apply_opacity(emoji1_canvas, opacity1)
|
|
76
|
+
|
|
77
|
+
# Create second emoji
|
|
78
|
+
emoji2_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
|
79
|
+
size2 = object2_data['size']
|
|
80
|
+
draw_emoji_enhanced(
|
|
81
|
+
emoji2_canvas,
|
|
82
|
+
emoji=object2_data['emoji'],
|
|
83
|
+
position=(center_pos[0] - size2 // 2, center_pos[1] - size2 // 2),
|
|
84
|
+
size=size2,
|
|
85
|
+
shadow=False
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
emoji2_canvas = apply_opacity(emoji2_canvas, opacity2)
|
|
89
|
+
|
|
90
|
+
# Composite both
|
|
91
|
+
frame_rgba = frame.convert('RGBA')
|
|
92
|
+
frame_rgba = Image.alpha_composite(frame_rgba, emoji1_canvas)
|
|
93
|
+
frame_rgba = Image.alpha_composite(frame_rgba, emoji2_canvas)
|
|
94
|
+
frame = frame_rgba.convert('RGB')
|
|
95
|
+
|
|
96
|
+
elif object_type == 'circle':
|
|
97
|
+
# Morph between two circles
|
|
98
|
+
radius1 = object1_data['radius']
|
|
99
|
+
radius2 = object2_data['radius']
|
|
100
|
+
color1 = object1_data['color']
|
|
101
|
+
color2 = object2_data['color']
|
|
102
|
+
|
|
103
|
+
# Interpolate properties
|
|
104
|
+
current_radius = int(interpolate(radius1, radius2, t, easing))
|
|
105
|
+
current_color = tuple(
|
|
106
|
+
int(interpolate(color1[i], color2[i], t, easing))
|
|
107
|
+
for i in range(3)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
draw_circle(frame, center_pos, current_radius, fill_color=current_color)
|
|
111
|
+
|
|
112
|
+
elif morph_type == 'scale':
|
|
113
|
+
# First object scales down as second scales up
|
|
114
|
+
if object_type == 'emoji':
|
|
115
|
+
scale1 = interpolate(1.0, 0.0, t, easing)
|
|
116
|
+
scale2 = interpolate(0.0, 1.0, t, easing)
|
|
117
|
+
|
|
118
|
+
# Draw first emoji (shrinking)
|
|
119
|
+
if scale1 > 0.05:
|
|
120
|
+
size1 = int(object1_data['size'] * scale1)
|
|
121
|
+
size1 = max(12, size1)
|
|
122
|
+
emoji1_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
|
123
|
+
draw_emoji_enhanced(
|
|
124
|
+
emoji1_canvas,
|
|
125
|
+
emoji=object1_data['emoji'],
|
|
126
|
+
position=(center_pos[0] - size1 // 2, center_pos[1] - size1 // 2),
|
|
127
|
+
size=size1,
|
|
128
|
+
shadow=False
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
frame_rgba = frame.convert('RGBA')
|
|
132
|
+
frame = Image.alpha_composite(frame_rgba, emoji1_canvas)
|
|
133
|
+
frame = frame.convert('RGB')
|
|
134
|
+
|
|
135
|
+
# Draw second emoji (growing)
|
|
136
|
+
if scale2 > 0.05:
|
|
137
|
+
size2 = int(object2_data['size'] * scale2)
|
|
138
|
+
size2 = max(12, size2)
|
|
139
|
+
emoji2_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
|
140
|
+
draw_emoji_enhanced(
|
|
141
|
+
emoji2_canvas,
|
|
142
|
+
emoji=object2_data['emoji'],
|
|
143
|
+
position=(center_pos[0] - size2 // 2, center_pos[1] - size2 // 2),
|
|
144
|
+
size=size2,
|
|
145
|
+
shadow=False
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
frame_rgba = frame.convert('RGBA')
|
|
149
|
+
frame = Image.alpha_composite(frame_rgba, emoji2_canvas)
|
|
150
|
+
frame = frame.convert('RGB')
|
|
151
|
+
|
|
152
|
+
elif morph_type == 'spin_morph':
|
|
153
|
+
# Spin while morphing (flip-like)
|
|
154
|
+
import math
|
|
155
|
+
|
|
156
|
+
# Calculate rotation (0 to 180 degrees)
|
|
157
|
+
angle = interpolate(0, 180, t, easing)
|
|
158
|
+
scale_factor = abs(math.cos(math.radians(angle)))
|
|
159
|
+
|
|
160
|
+
# Determine which object to show
|
|
161
|
+
if angle < 90:
|
|
162
|
+
current_object = object1_data
|
|
163
|
+
else:
|
|
164
|
+
current_object = object2_data
|
|
165
|
+
|
|
166
|
+
# Skip when edge-on
|
|
167
|
+
if scale_factor < 0.05:
|
|
168
|
+
frames.append(frame)
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
if object_type == 'emoji':
|
|
172
|
+
size = current_object['size']
|
|
173
|
+
canvas_size = size * 2
|
|
174
|
+
emoji_canvas = Image.new('RGBA', (canvas_size, canvas_size), (0, 0, 0, 0))
|
|
175
|
+
|
|
176
|
+
draw_emoji_enhanced(
|
|
177
|
+
emoji_canvas,
|
|
178
|
+
emoji=current_object['emoji'],
|
|
179
|
+
position=(canvas_size // 2 - size // 2, canvas_size // 2 - size // 2),
|
|
180
|
+
size=size,
|
|
181
|
+
shadow=False
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Scale horizontally for spin effect
|
|
185
|
+
new_width = max(1, int(canvas_size * scale_factor))
|
|
186
|
+
emoji_scaled = emoji_canvas.resize((new_width, canvas_size), Image.LANCZOS)
|
|
187
|
+
|
|
188
|
+
paste_x = center_pos[0] - new_width // 2
|
|
189
|
+
paste_y = center_pos[1] - canvas_size // 2
|
|
190
|
+
|
|
191
|
+
frame_rgba = frame.convert('RGBA')
|
|
192
|
+
frame_rgba.paste(emoji_scaled, (paste_x, paste_y), emoji_scaled)
|
|
193
|
+
frame = frame_rgba.convert('RGB')
|
|
194
|
+
|
|
195
|
+
frames.append(frame)
|
|
196
|
+
|
|
197
|
+
return frames
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def create_reaction_morph(
|
|
201
|
+
emoji_start: str,
|
|
202
|
+
emoji_end: str,
|
|
203
|
+
num_frames: int = 20,
|
|
204
|
+
frame_size: int = 128
|
|
205
|
+
) -> list[Image.Image]:
|
|
206
|
+
"""
|
|
207
|
+
Create quick emoji reaction morph (for emoji GIFs).
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
emoji_start: Starting emoji
|
|
211
|
+
emoji_end: Ending emoji
|
|
212
|
+
num_frames: Number of frames
|
|
213
|
+
frame_size: Frame size (square)
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
List of frames
|
|
217
|
+
"""
|
|
218
|
+
return create_morph_animation(
|
|
219
|
+
object1_data={'emoji': emoji_start, 'size': 80},
|
|
220
|
+
object2_data={'emoji': emoji_end, 'size': 80},
|
|
221
|
+
num_frames=num_frames,
|
|
222
|
+
morph_type='crossfade',
|
|
223
|
+
easing='ease_in_out',
|
|
224
|
+
object_type='emoji',
|
|
225
|
+
center_pos=(frame_size // 2, frame_size // 2),
|
|
226
|
+
frame_width=frame_size,
|
|
227
|
+
frame_height=frame_size,
|
|
228
|
+
bg_color=(255, 255, 255)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def create_shape_morph(
|
|
233
|
+
shapes: list[dict],
|
|
234
|
+
num_frames: int = 60,
|
|
235
|
+
frames_per_shape: int = 20,
|
|
236
|
+
frame_width: int = 480,
|
|
237
|
+
frame_height: int = 480,
|
|
238
|
+
bg_color: tuple[int, int, int] = (255, 255, 255)
|
|
239
|
+
) -> list[Image.Image]:
|
|
240
|
+
"""
|
|
241
|
+
Morph through a sequence of shapes.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
shapes: List of shape dicts with 'radius' and 'color'
|
|
245
|
+
num_frames: Total number of frames
|
|
246
|
+
frames_per_shape: Frames to spend on each morph
|
|
247
|
+
frame_width: Frame width
|
|
248
|
+
frame_height: Frame height
|
|
249
|
+
bg_color: Background color
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
List of frames
|
|
253
|
+
"""
|
|
254
|
+
frames = []
|
|
255
|
+
center = (frame_width // 2, frame_height // 2)
|
|
256
|
+
|
|
257
|
+
for i in range(num_frames):
|
|
258
|
+
# Determine which shapes we're morphing between
|
|
259
|
+
cycle_progress = (i % (frames_per_shape * len(shapes))) / frames_per_shape
|
|
260
|
+
shape_idx = int(cycle_progress) % len(shapes)
|
|
261
|
+
next_shape_idx = (shape_idx + 1) % len(shapes)
|
|
262
|
+
|
|
263
|
+
# Progress between these two shapes
|
|
264
|
+
t = cycle_progress - shape_idx
|
|
265
|
+
|
|
266
|
+
shape1 = shapes[shape_idx]
|
|
267
|
+
shape2 = shapes[next_shape_idx]
|
|
268
|
+
|
|
269
|
+
# Interpolate properties
|
|
270
|
+
radius = int(interpolate(shape1['radius'], shape2['radius'], t, 'ease_in_out'))
|
|
271
|
+
color = tuple(
|
|
272
|
+
int(interpolate(shape1['color'][j], shape2['color'][j], t, 'ease_in_out'))
|
|
273
|
+
for j in range(3)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Draw frame
|
|
277
|
+
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
278
|
+
draw_circle(frame, center, radius, fill_color=color)
|
|
279
|
+
|
|
280
|
+
frames.append(frame)
|
|
281
|
+
|
|
282
|
+
return frames
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# Example usage
|
|
286
|
+
if __name__ == '__main__':
|
|
287
|
+
print("Creating morph animations...")
|
|
288
|
+
|
|
289
|
+
builder = GIFBuilder(width=480, height=480, fps=20)
|
|
290
|
+
|
|
291
|
+
# Example 1: Crossfade morph
|
|
292
|
+
frames = create_morph_animation(
|
|
293
|
+
object1_data={'emoji': '😊', 'size': 100},
|
|
294
|
+
object2_data={'emoji': '😂', 'size': 100},
|
|
295
|
+
num_frames=30,
|
|
296
|
+
morph_type='crossfade',
|
|
297
|
+
object_type='emoji'
|
|
298
|
+
)
|
|
299
|
+
builder.add_frames(frames)
|
|
300
|
+
builder.save('morph_crossfade.gif', num_colors=128)
|
|
301
|
+
|
|
302
|
+
# Example 2: Scale morph
|
|
303
|
+
builder.clear()
|
|
304
|
+
frames = create_morph_animation(
|
|
305
|
+
object1_data={'emoji': '🌙', 'size': 100},
|
|
306
|
+
object2_data={'emoji': '☀️', 'size': 100},
|
|
307
|
+
num_frames=40,
|
|
308
|
+
morph_type='scale',
|
|
309
|
+
object_type='emoji'
|
|
310
|
+
)
|
|
311
|
+
builder.add_frames(frames)
|
|
312
|
+
builder.save('morph_scale.gif', num_colors=128)
|
|
313
|
+
|
|
314
|
+
# Example 3: Shape morph cycle
|
|
315
|
+
builder.clear()
|
|
316
|
+
from core.color_palettes import get_palette
|
|
317
|
+
palette = get_palette('vibrant')
|
|
318
|
+
|
|
319
|
+
shapes = [
|
|
320
|
+
{'radius': 60, 'color': palette['primary']},
|
|
321
|
+
{'radius': 80, 'color': palette['secondary']},
|
|
322
|
+
{'radius': 50, 'color': palette['accent']},
|
|
323
|
+
{'radius': 70, 'color': palette['success']}
|
|
324
|
+
]
|
|
325
|
+
frames = create_shape_morph(shapes, num_frames=80, frames_per_shape=20)
|
|
326
|
+
builder.add_frames(frames)
|
|
327
|
+
builder.save('morph_shapes.gif', num_colors=64)
|
|
328
|
+
|
|
329
|
+
print("Created morph animations!")
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Move Animation - Move objects along paths with various motion types.
|
|
4
|
+
|
|
5
|
+
Provides flexible movement primitives for objects along linear, arc, or custom paths.
|
|
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 core.gif_builder import GIFBuilder
|
|
15
|
+
from core.frame_composer import create_blank_frame, draw_circle, draw_emoji_enhanced
|
|
16
|
+
from core.easing import interpolate, calculate_arc_motion
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_move_animation(
|
|
20
|
+
object_type: str = 'emoji',
|
|
21
|
+
object_data: dict | None = None,
|
|
22
|
+
start_pos: tuple[int, int] = (50, 240),
|
|
23
|
+
end_pos: tuple[int, int] = (430, 240),
|
|
24
|
+
num_frames: int = 30,
|
|
25
|
+
motion_type: str = 'linear', # 'linear', 'arc', 'bezier', 'circle', 'wave'
|
|
26
|
+
easing: str = 'ease_out',
|
|
27
|
+
motion_params: dict | None = None,
|
|
28
|
+
frame_width: int = 480,
|
|
29
|
+
frame_height: int = 480,
|
|
30
|
+
bg_color: tuple[int, int, int] = (255, 255, 255)
|
|
31
|
+
) -> list:
|
|
32
|
+
"""
|
|
33
|
+
Create frames showing object moving along a path.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
object_type: 'circle', 'emoji', or 'custom'
|
|
37
|
+
object_data: Data for the object
|
|
38
|
+
start_pos: Starting (x, y) position
|
|
39
|
+
end_pos: Ending (x, y) position
|
|
40
|
+
num_frames: Number of frames
|
|
41
|
+
motion_type: Type of motion path
|
|
42
|
+
easing: Easing function name
|
|
43
|
+
motion_params: Additional parameters for motion (e.g., {'arc_height': 100})
|
|
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 == 'circle':
|
|
56
|
+
object_data = {'radius': 30, 'color': (100, 150, 255)}
|
|
57
|
+
elif object_type == 'emoji':
|
|
58
|
+
object_data = {'emoji': '🚀', 'size': 60}
|
|
59
|
+
|
|
60
|
+
# Default motion params
|
|
61
|
+
if motion_params is None:
|
|
62
|
+
motion_params = {}
|
|
63
|
+
|
|
64
|
+
for i in range(num_frames):
|
|
65
|
+
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
66
|
+
|
|
67
|
+
t = i / (num_frames - 1) if num_frames > 1 else 0
|
|
68
|
+
|
|
69
|
+
# Calculate position based on motion type
|
|
70
|
+
if motion_type == 'linear':
|
|
71
|
+
# Straight line with easing
|
|
72
|
+
x = interpolate(start_pos[0], end_pos[0], t, easing)
|
|
73
|
+
y = interpolate(start_pos[1], end_pos[1], t, easing)
|
|
74
|
+
|
|
75
|
+
elif motion_type == 'arc':
|
|
76
|
+
# Parabolic arc
|
|
77
|
+
arc_height = motion_params.get('arc_height', 100)
|
|
78
|
+
x, y = calculate_arc_motion(start_pos, end_pos, arc_height, t)
|
|
79
|
+
|
|
80
|
+
elif motion_type == 'circle':
|
|
81
|
+
# Circular motion around a center
|
|
82
|
+
center = motion_params.get('center', (frame_width // 2, frame_height // 2))
|
|
83
|
+
radius = motion_params.get('radius', 150)
|
|
84
|
+
start_angle = motion_params.get('start_angle', 0)
|
|
85
|
+
angle_range = motion_params.get('angle_range', 360) # Full circle
|
|
86
|
+
|
|
87
|
+
angle = start_angle + (angle_range * t)
|
|
88
|
+
angle_rad = math.radians(angle)
|
|
89
|
+
|
|
90
|
+
x = center[0] + radius * math.cos(angle_rad)
|
|
91
|
+
y = center[1] + radius * math.sin(angle_rad)
|
|
92
|
+
|
|
93
|
+
elif motion_type == 'wave':
|
|
94
|
+
# Move in straight line but add wave motion
|
|
95
|
+
wave_amplitude = motion_params.get('wave_amplitude', 50)
|
|
96
|
+
wave_frequency = motion_params.get('wave_frequency', 2)
|
|
97
|
+
|
|
98
|
+
# Base linear motion
|
|
99
|
+
base_x = interpolate(start_pos[0], end_pos[0], t, easing)
|
|
100
|
+
base_y = interpolate(start_pos[1], end_pos[1], t, easing)
|
|
101
|
+
|
|
102
|
+
# Add wave offset perpendicular to motion direction
|
|
103
|
+
dx = end_pos[0] - start_pos[0]
|
|
104
|
+
dy = end_pos[1] - start_pos[1]
|
|
105
|
+
length = math.sqrt(dx * dx + dy * dy)
|
|
106
|
+
|
|
107
|
+
if length > 0:
|
|
108
|
+
# Perpendicular direction
|
|
109
|
+
perp_x = -dy / length
|
|
110
|
+
perp_y = dx / length
|
|
111
|
+
|
|
112
|
+
# Wave offset
|
|
113
|
+
wave_offset = math.sin(t * wave_frequency * 2 * math.pi) * wave_amplitude
|
|
114
|
+
|
|
115
|
+
x = base_x + perp_x * wave_offset
|
|
116
|
+
y = base_y + perp_y * wave_offset
|
|
117
|
+
else:
|
|
118
|
+
x, y = base_x, base_y
|
|
119
|
+
|
|
120
|
+
elif motion_type == 'bezier':
|
|
121
|
+
# Quadratic bezier curve
|
|
122
|
+
control_point = motion_params.get('control_point', (
|
|
123
|
+
(start_pos[0] + end_pos[0]) // 2,
|
|
124
|
+
(start_pos[1] + end_pos[1]) // 2 - 100
|
|
125
|
+
))
|
|
126
|
+
|
|
127
|
+
# Quadratic Bezier formula: B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2
|
|
128
|
+
x = (1 - t) ** 2 * start_pos[0] + 2 * (1 - t) * t * control_point[0] + t ** 2 * end_pos[0]
|
|
129
|
+
y = (1 - t) ** 2 * start_pos[1] + 2 * (1 - t) * t * control_point[1] + t ** 2 * end_pos[1]
|
|
130
|
+
|
|
131
|
+
else:
|
|
132
|
+
# Default to linear
|
|
133
|
+
x = interpolate(start_pos[0], end_pos[0], t, easing)
|
|
134
|
+
y = interpolate(start_pos[1], end_pos[1], t, easing)
|
|
135
|
+
|
|
136
|
+
# Draw object at calculated position
|
|
137
|
+
x, y = int(x), int(y)
|
|
138
|
+
|
|
139
|
+
if object_type == 'circle':
|
|
140
|
+
draw_circle(
|
|
141
|
+
frame,
|
|
142
|
+
center=(x, y),
|
|
143
|
+
radius=object_data['radius'],
|
|
144
|
+
fill_color=object_data['color']
|
|
145
|
+
)
|
|
146
|
+
elif object_type == 'emoji':
|
|
147
|
+
draw_emoji_enhanced(
|
|
148
|
+
frame,
|
|
149
|
+
emoji=object_data['emoji'],
|
|
150
|
+
position=(x - object_data['size'] // 2, y - object_data['size'] // 2),
|
|
151
|
+
size=object_data['size'],
|
|
152
|
+
shadow=object_data.get('shadow', True)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
frames.append(frame)
|
|
156
|
+
|
|
157
|
+
return frames
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def create_path_from_points(points: list[tuple[int, int]],
|
|
161
|
+
num_frames: int = 60,
|
|
162
|
+
easing: str = 'ease_in_out') -> list[tuple[int, int]]:
|
|
163
|
+
"""
|
|
164
|
+
Create a smooth path through multiple points.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
points: List of (x, y) waypoints
|
|
168
|
+
num_frames: Total number of frames
|
|
169
|
+
easing: Easing between points
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
List of (x, y) positions for each frame
|
|
173
|
+
"""
|
|
174
|
+
if len(points) < 2:
|
|
175
|
+
return points * num_frames
|
|
176
|
+
|
|
177
|
+
path = []
|
|
178
|
+
frames_per_segment = num_frames // (len(points) - 1)
|
|
179
|
+
|
|
180
|
+
for i in range(len(points) - 1):
|
|
181
|
+
start = points[i]
|
|
182
|
+
end = points[i + 1]
|
|
183
|
+
|
|
184
|
+
# Last segment gets remaining frames
|
|
185
|
+
if i == len(points) - 2:
|
|
186
|
+
segment_frames = num_frames - len(path)
|
|
187
|
+
else:
|
|
188
|
+
segment_frames = frames_per_segment
|
|
189
|
+
|
|
190
|
+
for j in range(segment_frames):
|
|
191
|
+
t = j / segment_frames if segment_frames > 0 else 0
|
|
192
|
+
x = interpolate(start[0], end[0], t, easing)
|
|
193
|
+
y = interpolate(start[1], end[1], t, easing)
|
|
194
|
+
path.append((int(x), int(y)))
|
|
195
|
+
|
|
196
|
+
return path
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def apply_trail_effect(frames: list, trail_length: int = 5,
|
|
200
|
+
fade_alpha: float = 0.3) -> list:
|
|
201
|
+
"""
|
|
202
|
+
Add motion trail effect to moving object.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
frames: List of frames with moving object
|
|
206
|
+
trail_length: Number of previous frames to blend
|
|
207
|
+
fade_alpha: Opacity of trail frames
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
List of frames with trail effect
|
|
211
|
+
"""
|
|
212
|
+
from PIL import Image, ImageChops
|
|
213
|
+
import numpy as np
|
|
214
|
+
|
|
215
|
+
trailed_frames = []
|
|
216
|
+
|
|
217
|
+
for i, frame in enumerate(frames):
|
|
218
|
+
# Start with current frame
|
|
219
|
+
result = frame.copy()
|
|
220
|
+
|
|
221
|
+
# Blend previous frames
|
|
222
|
+
for j in range(1, min(trail_length + 1, i + 1)):
|
|
223
|
+
prev_frame = frames[i - j]
|
|
224
|
+
|
|
225
|
+
# Calculate fade
|
|
226
|
+
alpha = fade_alpha ** j
|
|
227
|
+
|
|
228
|
+
# Blend
|
|
229
|
+
result_array = np.array(result, dtype=np.float32)
|
|
230
|
+
prev_array = np.array(prev_frame, dtype=np.float32)
|
|
231
|
+
|
|
232
|
+
blended = result_array * (1 - alpha) + prev_array * alpha
|
|
233
|
+
result = Image.fromarray(blended.astype(np.uint8))
|
|
234
|
+
|
|
235
|
+
trailed_frames.append(result)
|
|
236
|
+
|
|
237
|
+
return trailed_frames
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# Example usage
|
|
241
|
+
if __name__ == '__main__':
|
|
242
|
+
print("Creating movement examples...")
|
|
243
|
+
|
|
244
|
+
# Example 1: Linear movement
|
|
245
|
+
builder = GIFBuilder(width=480, height=480, fps=20)
|
|
246
|
+
frames = create_move_animation(
|
|
247
|
+
object_type='emoji',
|
|
248
|
+
object_data={'emoji': '🚀', 'size': 60},
|
|
249
|
+
start_pos=(50, 240),
|
|
250
|
+
end_pos=(430, 240),
|
|
251
|
+
num_frames=30,
|
|
252
|
+
motion_type='linear',
|
|
253
|
+
easing='ease_out'
|
|
254
|
+
)
|
|
255
|
+
builder.add_frames(frames)
|
|
256
|
+
builder.save('move_linear.gif', num_colors=128)
|
|
257
|
+
|
|
258
|
+
# Example 2: Arc movement
|
|
259
|
+
builder.clear()
|
|
260
|
+
frames = create_move_animation(
|
|
261
|
+
object_type='emoji',
|
|
262
|
+
object_data={'emoji': '⚽', 'size': 60},
|
|
263
|
+
start_pos=(50, 350),
|
|
264
|
+
end_pos=(430, 350),
|
|
265
|
+
num_frames=30,
|
|
266
|
+
motion_type='arc',
|
|
267
|
+
motion_params={'arc_height': 150},
|
|
268
|
+
easing='linear'
|
|
269
|
+
)
|
|
270
|
+
builder.add_frames(frames)
|
|
271
|
+
builder.save('move_arc.gif', num_colors=128)
|
|
272
|
+
|
|
273
|
+
# Example 3: Circular movement
|
|
274
|
+
builder.clear()
|
|
275
|
+
frames = create_move_animation(
|
|
276
|
+
object_type='emoji',
|
|
277
|
+
object_data={'emoji': '🌍', 'size': 50},
|
|
278
|
+
start_pos=(0, 0), # Ignored for circle
|
|
279
|
+
end_pos=(0, 0), # Ignored for circle
|
|
280
|
+
num_frames=40,
|
|
281
|
+
motion_type='circle',
|
|
282
|
+
motion_params={
|
|
283
|
+
'center': (240, 240),
|
|
284
|
+
'radius': 120,
|
|
285
|
+
'start_angle': 0,
|
|
286
|
+
'angle_range': 360
|
|
287
|
+
},
|
|
288
|
+
easing='linear'
|
|
289
|
+
)
|
|
290
|
+
builder.add_frames(frames)
|
|
291
|
+
builder.save('move_circle.gif', num_colors=128)
|
|
292
|
+
|
|
293
|
+
print("Created movement examples!")
|