aico-cli 0.4.1 → 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/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,331 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Explode Animation - Break objects into pieces that fly outward.
|
|
4
|
+
|
|
5
|
+
Creates explosion, shatter, and particle burst effects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import math
|
|
11
|
+
import random
|
|
12
|
+
|
|
13
|
+
sys.path.append(str(Path(__file__).parent.parent))
|
|
14
|
+
|
|
15
|
+
from PIL import Image, ImageDraw
|
|
16
|
+
import numpy as np
|
|
17
|
+
from core.gif_builder import GIFBuilder
|
|
18
|
+
from core.frame_composer import create_blank_frame, draw_emoji_enhanced
|
|
19
|
+
from core.visual_effects import ParticleSystem
|
|
20
|
+
from core.easing import interpolate
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_explode_animation(
|
|
24
|
+
object_type: str = 'emoji',
|
|
25
|
+
object_data: dict | None = None,
|
|
26
|
+
num_frames: int = 30,
|
|
27
|
+
explode_type: str = 'burst', # 'burst', 'shatter', 'dissolve', 'implode'
|
|
28
|
+
num_pieces: int = 20,
|
|
29
|
+
explosion_speed: float = 5.0,
|
|
30
|
+
center_pos: tuple[int, int] = (240, 240),
|
|
31
|
+
frame_width: int = 480,
|
|
32
|
+
frame_height: int = 480,
|
|
33
|
+
bg_color: tuple[int, int, int] = (255, 255, 255)
|
|
34
|
+
) -> list[Image.Image]:
|
|
35
|
+
"""
|
|
36
|
+
Create explosion animation.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
object_type: 'emoji', 'circle', 'text'
|
|
40
|
+
object_data: Object configuration
|
|
41
|
+
num_frames: Number of frames
|
|
42
|
+
explode_type: Type of explosion
|
|
43
|
+
num_pieces: Number of pieces/particles
|
|
44
|
+
explosion_speed: Speed of explosion
|
|
45
|
+
center_pos: Center position
|
|
46
|
+
frame_width: Frame width
|
|
47
|
+
frame_height: Frame height
|
|
48
|
+
bg_color: Background color
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of frames
|
|
52
|
+
"""
|
|
53
|
+
frames = []
|
|
54
|
+
|
|
55
|
+
# Default object data
|
|
56
|
+
if object_data is None:
|
|
57
|
+
if object_type == 'emoji':
|
|
58
|
+
object_data = {'emoji': '💣', 'size': 100}
|
|
59
|
+
|
|
60
|
+
# Generate pieces/particles
|
|
61
|
+
pieces = []
|
|
62
|
+
for _ in range(num_pieces):
|
|
63
|
+
angle = random.uniform(0, 2 * math.pi)
|
|
64
|
+
speed = random.uniform(explosion_speed * 0.5, explosion_speed * 1.5)
|
|
65
|
+
vx = math.cos(angle) * speed
|
|
66
|
+
vy = math.sin(angle) * speed
|
|
67
|
+
size = random.randint(3, 12)
|
|
68
|
+
color = (
|
|
69
|
+
random.randint(100, 255),
|
|
70
|
+
random.randint(100, 255),
|
|
71
|
+
random.randint(100, 255)
|
|
72
|
+
)
|
|
73
|
+
rotation_speed = random.uniform(-20, 20)
|
|
74
|
+
|
|
75
|
+
pieces.append({
|
|
76
|
+
'vx': vx,
|
|
77
|
+
'vy': vy,
|
|
78
|
+
'size': size,
|
|
79
|
+
'color': color,
|
|
80
|
+
'rotation': 0,
|
|
81
|
+
'rotation_speed': rotation_speed
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
for i in range(num_frames):
|
|
85
|
+
t = i / (num_frames - 1) if num_frames > 1 else 0
|
|
86
|
+
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
87
|
+
draw = ImageDraw.Draw(frame)
|
|
88
|
+
|
|
89
|
+
if explode_type == 'burst':
|
|
90
|
+
# Show object at start, then explode
|
|
91
|
+
if t < 0.2:
|
|
92
|
+
# Object still intact
|
|
93
|
+
scale = interpolate(1.0, 1.2, t / 0.2, 'ease_out')
|
|
94
|
+
if object_type == 'emoji':
|
|
95
|
+
size = int(object_data['size'] * scale)
|
|
96
|
+
draw_emoji_enhanced(
|
|
97
|
+
frame,
|
|
98
|
+
emoji=object_data['emoji'],
|
|
99
|
+
position=(center_pos[0] - size // 2, center_pos[1] - size // 2),
|
|
100
|
+
size=size,
|
|
101
|
+
shadow=False
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
# Exploded - draw pieces
|
|
105
|
+
explosion_t = (t - 0.2) / 0.8
|
|
106
|
+
for piece in pieces:
|
|
107
|
+
# Update position
|
|
108
|
+
x = center_pos[0] + piece['vx'] * explosion_t * 50
|
|
109
|
+
y = center_pos[1] + piece['vy'] * explosion_t * 50 + 0.5 * 300 * explosion_t ** 2 # Gravity
|
|
110
|
+
|
|
111
|
+
# Fade out
|
|
112
|
+
alpha = 1.0 - explosion_t
|
|
113
|
+
if alpha > 0:
|
|
114
|
+
color = tuple(int(c * alpha) for c in piece['color'])
|
|
115
|
+
size = int(piece['size'] * (1 - explosion_t * 0.5))
|
|
116
|
+
|
|
117
|
+
draw.ellipse(
|
|
118
|
+
[x - size, y - size, x + size, y + size],
|
|
119
|
+
fill=color
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
elif explode_type == 'shatter':
|
|
123
|
+
# Break into geometric pieces
|
|
124
|
+
if t < 0.15:
|
|
125
|
+
# Object intact
|
|
126
|
+
if object_type == 'emoji':
|
|
127
|
+
draw_emoji_enhanced(
|
|
128
|
+
frame,
|
|
129
|
+
emoji=object_data['emoji'],
|
|
130
|
+
position=(center_pos[0] - object_data['size'] // 2,
|
|
131
|
+
center_pos[1] - object_data['size'] // 2),
|
|
132
|
+
size=object_data['size'],
|
|
133
|
+
shadow=False
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
# Shattered
|
|
137
|
+
shatter_t = (t - 0.15) / 0.85
|
|
138
|
+
|
|
139
|
+
# Draw triangular shards
|
|
140
|
+
for piece in pieces[:min(10, len(pieces))]:
|
|
141
|
+
x = center_pos[0] + piece['vx'] * shatter_t * 30
|
|
142
|
+
y = center_pos[1] + piece['vy'] * shatter_t * 30 + 0.5 * 200 * shatter_t ** 2
|
|
143
|
+
|
|
144
|
+
# Update rotation
|
|
145
|
+
rotation = piece['rotation_speed'] * shatter_t * 100
|
|
146
|
+
|
|
147
|
+
# Draw triangle shard
|
|
148
|
+
shard_size = piece['size'] * 2
|
|
149
|
+
points = []
|
|
150
|
+
for j in range(3):
|
|
151
|
+
angle = (rotation + j * 120) * math.pi / 180
|
|
152
|
+
px = x + shard_size * math.cos(angle)
|
|
153
|
+
py = y + shard_size * math.sin(angle)
|
|
154
|
+
points.append((px, py))
|
|
155
|
+
|
|
156
|
+
alpha = 1.0 - shatter_t
|
|
157
|
+
if alpha > 0:
|
|
158
|
+
color = tuple(int(c * alpha) for c in piece['color'])
|
|
159
|
+
draw.polygon(points, fill=color)
|
|
160
|
+
|
|
161
|
+
elif explode_type == 'dissolve':
|
|
162
|
+
# Dissolve into particles
|
|
163
|
+
dissolve_scale = interpolate(1.0, 0.0, t, 'ease_in')
|
|
164
|
+
|
|
165
|
+
if dissolve_scale > 0.1:
|
|
166
|
+
# Draw fading object
|
|
167
|
+
if object_type == 'emoji':
|
|
168
|
+
size = int(object_data['size'] * dissolve_scale)
|
|
169
|
+
size = max(12, size)
|
|
170
|
+
|
|
171
|
+
emoji_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
|
172
|
+
draw_emoji_enhanced(
|
|
173
|
+
emoji_canvas,
|
|
174
|
+
emoji=object_data['emoji'],
|
|
175
|
+
position=(center_pos[0] - size // 2, center_pos[1] - size // 2),
|
|
176
|
+
size=size,
|
|
177
|
+
shadow=False
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Apply opacity
|
|
181
|
+
from templates.fade import apply_opacity
|
|
182
|
+
emoji_canvas = apply_opacity(emoji_canvas, dissolve_scale)
|
|
183
|
+
|
|
184
|
+
frame_rgba = frame.convert('RGBA')
|
|
185
|
+
frame = Image.alpha_composite(frame_rgba, emoji_canvas)
|
|
186
|
+
frame = frame.convert('RGB')
|
|
187
|
+
draw = ImageDraw.Draw(frame)
|
|
188
|
+
|
|
189
|
+
# Draw outward-moving particles
|
|
190
|
+
for piece in pieces:
|
|
191
|
+
x = center_pos[0] + piece['vx'] * t * 40
|
|
192
|
+
y = center_pos[1] + piece['vy'] * t * 40
|
|
193
|
+
|
|
194
|
+
alpha = 1.0 - t
|
|
195
|
+
if alpha > 0:
|
|
196
|
+
color = tuple(int(c * alpha) for c in piece['color'])
|
|
197
|
+
size = int(piece['size'] * (1 - t * 0.5))
|
|
198
|
+
draw.ellipse(
|
|
199
|
+
[x - size, y - size, x + size, y + size],
|
|
200
|
+
fill=color
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
elif explode_type == 'implode':
|
|
204
|
+
# Reverse explosion - pieces fly inward
|
|
205
|
+
if t < 0.7:
|
|
206
|
+
# Pieces converging
|
|
207
|
+
implode_t = 1.0 - (t / 0.7)
|
|
208
|
+
for piece in pieces:
|
|
209
|
+
x = center_pos[0] + piece['vx'] * implode_t * 50
|
|
210
|
+
y = center_pos[1] + piece['vy'] * implode_t * 50
|
|
211
|
+
|
|
212
|
+
alpha = 1.0 - (1.0 - implode_t) * 0.5
|
|
213
|
+
color = tuple(int(c * alpha) for c in piece['color'])
|
|
214
|
+
size = int(piece['size'] * alpha)
|
|
215
|
+
|
|
216
|
+
draw.ellipse(
|
|
217
|
+
[x - size, y - size, x + size, y + size],
|
|
218
|
+
fill=color
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
# Object reforms
|
|
222
|
+
reform_t = (t - 0.7) / 0.3
|
|
223
|
+
scale = interpolate(0.5, 1.0, reform_t, 'elastic_out')
|
|
224
|
+
|
|
225
|
+
if object_type == 'emoji':
|
|
226
|
+
size = int(object_data['size'] * scale)
|
|
227
|
+
draw_emoji_enhanced(
|
|
228
|
+
frame,
|
|
229
|
+
emoji=object_data['emoji'],
|
|
230
|
+
position=(center_pos[0] - size // 2, center_pos[1] - size // 2),
|
|
231
|
+
size=size,
|
|
232
|
+
shadow=False
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
frames.append(frame)
|
|
236
|
+
|
|
237
|
+
return frames
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def create_particle_burst(
|
|
241
|
+
num_frames: int = 25,
|
|
242
|
+
particle_count: int = 30,
|
|
243
|
+
center_pos: tuple[int, int] = (240, 240),
|
|
244
|
+
colors: list[tuple[int, int, int]] | None = None,
|
|
245
|
+
frame_width: int = 480,
|
|
246
|
+
frame_height: int = 480,
|
|
247
|
+
bg_color: tuple[int, int, int] = (255, 255, 255)
|
|
248
|
+
) -> list[Image.Image]:
|
|
249
|
+
"""
|
|
250
|
+
Create simple particle burst effect.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
num_frames: Number of frames
|
|
254
|
+
particle_count: Number of particles
|
|
255
|
+
center_pos: Burst center
|
|
256
|
+
colors: Particle colors (None for random)
|
|
257
|
+
frame_width: Frame width
|
|
258
|
+
frame_height: Frame height
|
|
259
|
+
bg_color: Background color
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
List of frames
|
|
263
|
+
"""
|
|
264
|
+
particles = ParticleSystem()
|
|
265
|
+
|
|
266
|
+
# Emit particles
|
|
267
|
+
if colors is None:
|
|
268
|
+
from core.color_palettes import get_palette
|
|
269
|
+
palette = get_palette('vibrant')
|
|
270
|
+
colors = [palette['primary'], palette['secondary'], palette['accent']]
|
|
271
|
+
|
|
272
|
+
for _ in range(particle_count):
|
|
273
|
+
color = random.choice(colors)
|
|
274
|
+
particles.emit(
|
|
275
|
+
center_pos[0], center_pos[1],
|
|
276
|
+
count=1,
|
|
277
|
+
speed=random.uniform(3, 8),
|
|
278
|
+
color=color,
|
|
279
|
+
lifetime=random.uniform(20, 30),
|
|
280
|
+
size=random.randint(3, 8),
|
|
281
|
+
shape='star'
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
frames = []
|
|
285
|
+
for _ in range(num_frames):
|
|
286
|
+
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
287
|
+
|
|
288
|
+
particles.update()
|
|
289
|
+
particles.render(frame)
|
|
290
|
+
|
|
291
|
+
frames.append(frame)
|
|
292
|
+
|
|
293
|
+
return frames
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Example usage
|
|
297
|
+
if __name__ == '__main__':
|
|
298
|
+
print("Creating explode animations...")
|
|
299
|
+
|
|
300
|
+
builder = GIFBuilder(width=480, height=480, fps=20)
|
|
301
|
+
|
|
302
|
+
# Example 1: Burst
|
|
303
|
+
frames = create_explode_animation(
|
|
304
|
+
object_type='emoji',
|
|
305
|
+
object_data={'emoji': '💣', 'size': 100},
|
|
306
|
+
num_frames=30,
|
|
307
|
+
explode_type='burst',
|
|
308
|
+
num_pieces=25
|
|
309
|
+
)
|
|
310
|
+
builder.add_frames(frames)
|
|
311
|
+
builder.save('explode_burst.gif', num_colors=128)
|
|
312
|
+
|
|
313
|
+
# Example 2: Shatter
|
|
314
|
+
builder.clear()
|
|
315
|
+
frames = create_explode_animation(
|
|
316
|
+
object_type='emoji',
|
|
317
|
+
object_data={'emoji': '🪟', 'size': 100},
|
|
318
|
+
num_frames=30,
|
|
319
|
+
explode_type='shatter',
|
|
320
|
+
num_pieces=12
|
|
321
|
+
)
|
|
322
|
+
builder.add_frames(frames)
|
|
323
|
+
builder.save('explode_shatter.gif', num_colors=128)
|
|
324
|
+
|
|
325
|
+
# Example 3: Particle burst
|
|
326
|
+
builder.clear()
|
|
327
|
+
frames = create_particle_burst(num_frames=25, particle_count=40)
|
|
328
|
+
builder.add_frames(frames)
|
|
329
|
+
builder.save('explode_particles.gif', num_colors=128)
|
|
330
|
+
|
|
331
|
+
print("Created explode animations!")
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Fade Animation - Fade in, fade out, and crossfade effects.
|
|
4
|
+
|
|
5
|
+
Creates smooth opacity transitions for appearing, disappearing, and transitioning.
|
|
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, ImageDraw
|
|
14
|
+
import numpy as np
|
|
15
|
+
from core.gif_builder import GIFBuilder
|
|
16
|
+
from core.frame_composer import create_blank_frame, draw_emoji_enhanced
|
|
17
|
+
from core.easing import interpolate
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_fade_animation(
|
|
21
|
+
object_type: str = 'emoji',
|
|
22
|
+
object_data: dict | None = None,
|
|
23
|
+
num_frames: int = 30,
|
|
24
|
+
fade_type: str = 'in', # 'in', 'out', 'in_out', 'blink'
|
|
25
|
+
easing: str = 'ease_in_out',
|
|
26
|
+
center_pos: tuple[int, int] = (240, 240),
|
|
27
|
+
frame_width: int = 480,
|
|
28
|
+
frame_height: int = 480,
|
|
29
|
+
bg_color: tuple[int, int, int] = (255, 255, 255)
|
|
30
|
+
) -> list[Image.Image]:
|
|
31
|
+
"""
|
|
32
|
+
Create fade animation.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
object_type: 'emoji', 'text', 'image'
|
|
36
|
+
object_data: Object configuration
|
|
37
|
+
num_frames: Number of frames
|
|
38
|
+
fade_type: Type of fade effect
|
|
39
|
+
easing: Easing function
|
|
40
|
+
center_pos: Center position
|
|
41
|
+
frame_width: Frame width
|
|
42
|
+
frame_height: Frame height
|
|
43
|
+
bg_color: Background color
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
List of frames
|
|
47
|
+
"""
|
|
48
|
+
frames = []
|
|
49
|
+
|
|
50
|
+
# Default object data
|
|
51
|
+
if object_data is None:
|
|
52
|
+
if object_type == 'emoji':
|
|
53
|
+
object_data = {'emoji': '✨', 'size': 100}
|
|
54
|
+
|
|
55
|
+
for i in range(num_frames):
|
|
56
|
+
t = i / (num_frames - 1) if num_frames > 1 else 0
|
|
57
|
+
|
|
58
|
+
# Calculate opacity based on fade type
|
|
59
|
+
if fade_type == 'in':
|
|
60
|
+
opacity = interpolate(0, 1, t, easing)
|
|
61
|
+
elif fade_type == 'out':
|
|
62
|
+
opacity = interpolate(1, 0, t, easing)
|
|
63
|
+
elif fade_type == 'in_out':
|
|
64
|
+
if t < 0.5:
|
|
65
|
+
opacity = interpolate(0, 1, t * 2, easing)
|
|
66
|
+
else:
|
|
67
|
+
opacity = interpolate(1, 0, (t - 0.5) * 2, easing)
|
|
68
|
+
elif fade_type == 'blink':
|
|
69
|
+
# Quick fade out and back in
|
|
70
|
+
if t < 0.2:
|
|
71
|
+
opacity = interpolate(1, 0, t / 0.2, 'ease_in')
|
|
72
|
+
elif t < 0.4:
|
|
73
|
+
opacity = interpolate(0, 1, (t - 0.2) / 0.2, 'ease_out')
|
|
74
|
+
else:
|
|
75
|
+
opacity = 1.0
|
|
76
|
+
else:
|
|
77
|
+
opacity = interpolate(0, 1, t, easing)
|
|
78
|
+
|
|
79
|
+
# Create background
|
|
80
|
+
frame_bg = create_blank_frame(frame_width, frame_height, bg_color)
|
|
81
|
+
|
|
82
|
+
# Create object layer with transparency
|
|
83
|
+
if object_type == 'emoji':
|
|
84
|
+
# Create RGBA canvas for emoji
|
|
85
|
+
emoji_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
|
86
|
+
emoji_size = object_data['size']
|
|
87
|
+
draw_emoji_enhanced(
|
|
88
|
+
emoji_canvas,
|
|
89
|
+
emoji=object_data['emoji'],
|
|
90
|
+
position=(center_pos[0] - emoji_size // 2, center_pos[1] - emoji_size // 2),
|
|
91
|
+
size=emoji_size,
|
|
92
|
+
shadow=object_data.get('shadow', False)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Apply opacity
|
|
96
|
+
emoji_canvas = apply_opacity(emoji_canvas, opacity)
|
|
97
|
+
|
|
98
|
+
# Composite onto background
|
|
99
|
+
frame_bg_rgba = frame_bg.convert('RGBA')
|
|
100
|
+
frame = Image.alpha_composite(frame_bg_rgba, emoji_canvas)
|
|
101
|
+
frame = frame.convert('RGB')
|
|
102
|
+
|
|
103
|
+
elif object_type == 'text':
|
|
104
|
+
from core.typography import draw_text_with_outline
|
|
105
|
+
|
|
106
|
+
# Create text on separate layer
|
|
107
|
+
text_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
|
108
|
+
text_canvas_rgb = text_canvas.convert('RGB')
|
|
109
|
+
text_canvas_rgb.paste(bg_color, (0, 0, frame_width, frame_height))
|
|
110
|
+
|
|
111
|
+
draw_text_with_outline(
|
|
112
|
+
text_canvas_rgb,
|
|
113
|
+
text=object_data.get('text', 'FADE'),
|
|
114
|
+
position=center_pos,
|
|
115
|
+
font_size=object_data.get('font_size', 60),
|
|
116
|
+
text_color=object_data.get('text_color', (0, 0, 0)),
|
|
117
|
+
outline_color=object_data.get('outline_color', (255, 255, 255)),
|
|
118
|
+
outline_width=3,
|
|
119
|
+
centered=True
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Convert to RGBA and make background transparent
|
|
123
|
+
text_canvas = text_canvas_rgb.convert('RGBA')
|
|
124
|
+
data = text_canvas.getdata()
|
|
125
|
+
new_data = []
|
|
126
|
+
for item in data:
|
|
127
|
+
if item[:3] == bg_color:
|
|
128
|
+
new_data.append((255, 255, 255, 0))
|
|
129
|
+
else:
|
|
130
|
+
new_data.append(item)
|
|
131
|
+
text_canvas.putdata(new_data)
|
|
132
|
+
|
|
133
|
+
# Apply opacity
|
|
134
|
+
text_canvas = apply_opacity(text_canvas, opacity)
|
|
135
|
+
|
|
136
|
+
# Composite
|
|
137
|
+
frame_bg_rgba = frame_bg.convert('RGBA')
|
|
138
|
+
frame = Image.alpha_composite(frame_bg_rgba, text_canvas)
|
|
139
|
+
frame = frame.convert('RGB')
|
|
140
|
+
|
|
141
|
+
else:
|
|
142
|
+
frame = frame_bg
|
|
143
|
+
|
|
144
|
+
frames.append(frame)
|
|
145
|
+
|
|
146
|
+
return frames
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def apply_opacity(image: Image.Image, opacity: float) -> Image.Image:
|
|
150
|
+
"""
|
|
151
|
+
Apply opacity to an RGBA image.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
image: RGBA image
|
|
155
|
+
opacity: Opacity value (0.0 to 1.0)
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Image with adjusted opacity
|
|
159
|
+
"""
|
|
160
|
+
if image.mode != 'RGBA':
|
|
161
|
+
image = image.convert('RGBA')
|
|
162
|
+
|
|
163
|
+
# Get alpha channel
|
|
164
|
+
r, g, b, a = image.split()
|
|
165
|
+
|
|
166
|
+
# Multiply alpha by opacity
|
|
167
|
+
a_array = np.array(a, dtype=np.float32)
|
|
168
|
+
a_array = a_array * opacity
|
|
169
|
+
a = Image.fromarray(a_array.astype(np.uint8))
|
|
170
|
+
|
|
171
|
+
# Merge back
|
|
172
|
+
return Image.merge('RGBA', (r, g, b, a))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def create_crossfade(
|
|
176
|
+
object1_data: dict,
|
|
177
|
+
object2_data: dict,
|
|
178
|
+
num_frames: int = 30,
|
|
179
|
+
easing: str = 'ease_in_out',
|
|
180
|
+
object_type: str = 'emoji',
|
|
181
|
+
center_pos: tuple[int, int] = (240, 240),
|
|
182
|
+
frame_width: int = 480,
|
|
183
|
+
frame_height: int = 480,
|
|
184
|
+
bg_color: tuple[int, int, int] = (255, 255, 255)
|
|
185
|
+
) -> list[Image.Image]:
|
|
186
|
+
"""
|
|
187
|
+
Crossfade between two objects.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
object1_data: First object configuration
|
|
191
|
+
object2_data: Second object configuration
|
|
192
|
+
num_frames: Number of frames
|
|
193
|
+
easing: Easing function
|
|
194
|
+
object_type: Type of objects
|
|
195
|
+
center_pos: Center position
|
|
196
|
+
frame_width: Frame width
|
|
197
|
+
frame_height: Frame height
|
|
198
|
+
bg_color: Background color
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
List of frames
|
|
202
|
+
"""
|
|
203
|
+
frames = []
|
|
204
|
+
|
|
205
|
+
for i in range(num_frames):
|
|
206
|
+
t = i / (num_frames - 1) if num_frames > 1 else 0
|
|
207
|
+
|
|
208
|
+
# Calculate opacities
|
|
209
|
+
opacity1 = interpolate(1, 0, t, easing)
|
|
210
|
+
opacity2 = interpolate(0, 1, t, easing)
|
|
211
|
+
|
|
212
|
+
# Create background
|
|
213
|
+
frame = create_blank_frame(frame_width, frame_height, bg_color)
|
|
214
|
+
|
|
215
|
+
if object_type == 'emoji':
|
|
216
|
+
# Create first emoji
|
|
217
|
+
emoji1_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
|
218
|
+
size1 = object1_data['size']
|
|
219
|
+
draw_emoji_enhanced(
|
|
220
|
+
emoji1_canvas,
|
|
221
|
+
emoji=object1_data['emoji'],
|
|
222
|
+
position=(center_pos[0] - size1 // 2, center_pos[1] - size1 // 2),
|
|
223
|
+
size=size1,
|
|
224
|
+
shadow=False
|
|
225
|
+
)
|
|
226
|
+
emoji1_canvas = apply_opacity(emoji1_canvas, opacity1)
|
|
227
|
+
|
|
228
|
+
# Create second emoji
|
|
229
|
+
emoji2_canvas = Image.new('RGBA', (frame_width, frame_height), (0, 0, 0, 0))
|
|
230
|
+
size2 = object2_data['size']
|
|
231
|
+
draw_emoji_enhanced(
|
|
232
|
+
emoji2_canvas,
|
|
233
|
+
emoji=object2_data['emoji'],
|
|
234
|
+
position=(center_pos[0] - size2 // 2, center_pos[1] - size2 // 2),
|
|
235
|
+
size=size2,
|
|
236
|
+
shadow=False
|
|
237
|
+
)
|
|
238
|
+
emoji2_canvas = apply_opacity(emoji2_canvas, opacity2)
|
|
239
|
+
|
|
240
|
+
# Composite both
|
|
241
|
+
frame_rgba = frame.convert('RGBA')
|
|
242
|
+
frame_rgba = Image.alpha_composite(frame_rgba, emoji1_canvas)
|
|
243
|
+
frame_rgba = Image.alpha_composite(frame_rgba, emoji2_canvas)
|
|
244
|
+
frame = frame_rgba.convert('RGB')
|
|
245
|
+
|
|
246
|
+
frames.append(frame)
|
|
247
|
+
|
|
248
|
+
return frames
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def create_fade_to_color(
|
|
252
|
+
start_color: tuple[int, int, int],
|
|
253
|
+
end_color: tuple[int, int, int],
|
|
254
|
+
num_frames: int = 20,
|
|
255
|
+
easing: str = 'linear',
|
|
256
|
+
frame_width: int = 480,
|
|
257
|
+
frame_height: int = 480
|
|
258
|
+
) -> list[Image.Image]:
|
|
259
|
+
"""
|
|
260
|
+
Fade from one solid color to another.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
start_color: Starting RGB color
|
|
264
|
+
end_color: Ending RGB color
|
|
265
|
+
num_frames: Number of frames
|
|
266
|
+
easing: Easing function
|
|
267
|
+
frame_width: Frame width
|
|
268
|
+
frame_height: Frame height
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List of frames
|
|
272
|
+
"""
|
|
273
|
+
frames = []
|
|
274
|
+
|
|
275
|
+
for i in range(num_frames):
|
|
276
|
+
t = i / (num_frames - 1) if num_frames > 1 else 0
|
|
277
|
+
|
|
278
|
+
# Interpolate each color channel
|
|
279
|
+
r = int(interpolate(start_color[0], end_color[0], t, easing))
|
|
280
|
+
g = int(interpolate(start_color[1], end_color[1], t, easing))
|
|
281
|
+
b = int(interpolate(start_color[2], end_color[2], t, easing))
|
|
282
|
+
|
|
283
|
+
color = (r, g, b)
|
|
284
|
+
frame = create_blank_frame(frame_width, frame_height, color)
|
|
285
|
+
frames.append(frame)
|
|
286
|
+
|
|
287
|
+
return frames
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# Example usage
|
|
291
|
+
if __name__ == '__main__':
|
|
292
|
+
print("Creating fade animations...")
|
|
293
|
+
|
|
294
|
+
builder = GIFBuilder(width=480, height=480, fps=20)
|
|
295
|
+
|
|
296
|
+
# Example 1: Fade in
|
|
297
|
+
frames = create_fade_animation(
|
|
298
|
+
object_type='emoji',
|
|
299
|
+
object_data={'emoji': '✨', 'size': 120},
|
|
300
|
+
num_frames=30,
|
|
301
|
+
fade_type='in',
|
|
302
|
+
easing='ease_out'
|
|
303
|
+
)
|
|
304
|
+
builder.add_frames(frames)
|
|
305
|
+
builder.save('fade_in.gif', num_colors=128)
|
|
306
|
+
|
|
307
|
+
# Example 2: Crossfade
|
|
308
|
+
builder.clear()
|
|
309
|
+
frames = create_crossfade(
|
|
310
|
+
object1_data={'emoji': '😊', 'size': 100},
|
|
311
|
+
object2_data={'emoji': '😂', 'size': 100},
|
|
312
|
+
num_frames=30,
|
|
313
|
+
object_type='emoji'
|
|
314
|
+
)
|
|
315
|
+
builder.add_frames(frames)
|
|
316
|
+
builder.save('fade_crossfade.gif', num_colors=128)
|
|
317
|
+
|
|
318
|
+
# Example 3: Blink
|
|
319
|
+
builder.clear()
|
|
320
|
+
frames = create_fade_animation(
|
|
321
|
+
object_type='emoji',
|
|
322
|
+
object_data={'emoji': '👀', 'size': 100},
|
|
323
|
+
num_frames=20,
|
|
324
|
+
fade_type='blink'
|
|
325
|
+
)
|
|
326
|
+
builder.add_frames(frames)
|
|
327
|
+
builder.save('fade_blink.gif', num_colors=128)
|
|
328
|
+
|
|
329
|
+
print("Created fade animations!")
|